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:
@@ -1213,8 +1213,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
userToken:
|
userToken:
|
||||||
type: string
|
type: string
|
||||||
priority:
|
|
||||||
type: number
|
|
||||||
LunaSeaSettings:
|
LunaSeaSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1599,6 +1597,9 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
discordEnabled:
|
discordEnabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
discordEnabledTypes:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
discordId:
|
discordId:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ export class UserSettings {
|
|||||||
})
|
})
|
||||||
public notificationTypes: Partial<NotificationAgentTypes>;
|
public notificationTypes: Partial<NotificationAgentTypes>;
|
||||||
|
|
||||||
public hasNotificationType(key: NotificationAgentKey, type: Notification) {
|
public hasNotificationType(
|
||||||
|
key: NotificationAgentKey,
|
||||||
|
type: Notification
|
||||||
|
): boolean {
|
||||||
return hasNotificationType(type, this.notificationTypes[key] ?? 0);
|
return hasNotificationType(type, this.notificationTypes[key] ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface UserSettingsNotificationsResponse {
|
|||||||
emailEnabled?: boolean;
|
emailEnabled?: boolean;
|
||||||
pgpKey?: string;
|
pgpKey?: string;
|
||||||
discordEnabled?: boolean;
|
discordEnabled?: boolean;
|
||||||
|
discordEnabledTypes?: number;
|
||||||
discordId?: string;
|
discordId?: string;
|
||||||
telegramEnabled?: boolean;
|
telegramEnabled?: boolean;
|
||||||
telegramBotUsername?: string;
|
telegramBotUsername?: string;
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ export abstract class BaseAgent<T extends NotificationAgentConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationAgent {
|
export interface NotificationAgent {
|
||||||
shouldSend(type: Notification): boolean;
|
shouldSend(): boolean;
|
||||||
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
|
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,12 +193,10 @@ class DiscordAgent
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
if (
|
const settings = this.getSettings();
|
||||||
this.getSettings().enabled &&
|
|
||||||
this.getSettings().options.webhookUrl &&
|
if (settings.enabled && settings.options.webhookUrl) {
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +207,12 @@ class DiscordAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Sending Discord notification', {
|
logger.debug('Sending Discord notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
@@ -218,13 +222,6 @@ class DiscordAgent
|
|||||||
let content = undefined;
|
let content = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { botUsername, botAvatarUrl, webhookUrl } =
|
|
||||||
this.getSettings().options;
|
|
||||||
|
|
||||||
if (!webhookUrl) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.notifyUser) {
|
if (payload.notifyUser) {
|
||||||
// Mention user who submitted the request
|
// Mention user who submitted the request
|
||||||
if (
|
if (
|
||||||
@@ -258,9 +255,9 @@ class DiscordAgent
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.post(webhookUrl, {
|
await axios.post(settings.options.webhookUrl, {
|
||||||
username: botUsername,
|
username: settings.options.botUsername,
|
||||||
avatar_url: botAvatarUrl,
|
avatar_url: settings.options.botAvatarUrl,
|
||||||
embeds: [this.buildEmbed(type, payload)],
|
embeds: [this.buildEmbed(type, payload)],
|
||||||
content,
|
content,
|
||||||
} as DiscordWebhookPayload);
|
} as DiscordWebhookPayload);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EmailOptions } from 'email-templates';
|
import { EmailOptions } from 'email-templates';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { Notification } from '..';
|
||||||
import { MediaType } from '../../../constants/media';
|
import { MediaType } from '../../../constants/media';
|
||||||
import { User } from '../../../entity/User';
|
import { User } from '../../../entity/User';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
@@ -28,12 +28,14 @@ class EmailAgent
|
|||||||
return settings.notifications.agents.email;
|
return settings.notifications.agents.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
const settings = this.getSettings();
|
const settings = this.getSettings();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settings.enabled &&
|
settings.enabled &&
|
||||||
hasNotificationType(type, this.getSettings().types)
|
settings.options.emailFrom &&
|
||||||
|
settings.options.smtpHost &&
|
||||||
|
settings.options.smtpPort
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,10 @@ class LunaSeaAgent
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
if (
|
const settings = this.getSettings();
|
||||||
this.getSettings().enabled &&
|
|
||||||
this.getSettings().options.webhookUrl &&
|
if (settings.enabled && settings.options.webhookUrl) {
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +64,12 @@ class LunaSeaAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Sending LunaSea notification', {
|
logger.debug('Sending LunaSea notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
@@ -73,19 +77,19 @@ class LunaSeaAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webhookUrl, profileName } = this.getSettings().options;
|
await axios.post(
|
||||||
|
settings.options.webhookUrl,
|
||||||
if (!webhookUrl) {
|
this.buildPayload(type, payload),
|
||||||
return false;
|
settings.options.profileName
|
||||||
}
|
? {
|
||||||
|
headers: {
|
||||||
await axios.post(webhookUrl, this.buildPayload(type, payload), {
|
Authorization: `Basic ${Buffer.from(
|
||||||
headers: {
|
`${settings.options.profileName}:`
|
||||||
Authorization: `Basic ${Buffer.from(`${profileName}:`).toString(
|
).toString('base64')}`,
|
||||||
'base64'
|
},
|
||||||
)}`,
|
}
|
||||||
},
|
: undefined
|
||||||
});
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -94,7 +98,7 @@ class LunaSeaAgent
|
|||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: e.response.data,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ class PushbulletAgent
|
|||||||
return settings.notifications.agents.pushbullet;
|
return settings.notifications.agents.pushbullet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
if (
|
const settings = this.getSettings();
|
||||||
this.getSettings().enabled &&
|
|
||||||
this.getSettings().options.accessToken &&
|
if (settings.enabled && settings.options.accessToken) {
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +135,12 @@ class PushbulletAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Sending Pushbullet notification', {
|
logger.debug('Sending Pushbullet notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
@@ -144,14 +148,10 @@ class PushbulletAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = 'https://api.pushbullet.com/v2/pushes';
|
|
||||||
|
|
||||||
const { accessToken } = this.getSettings().options;
|
|
||||||
|
|
||||||
const { title, body } = this.constructMessageDetails(type, payload);
|
const { title, body } = this.constructMessageDetails(type, payload);
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
endpoint,
|
'https://api.pushbullet.com/v2/pushes',
|
||||||
{
|
{
|
||||||
type: 'note',
|
type: 'note',
|
||||||
title: title,
|
title: title,
|
||||||
@@ -159,7 +159,7 @@ class PushbulletAgent
|
|||||||
} as PushbulletPayload,
|
} as PushbulletPayload,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Access-Token': accessToken,
|
'Access-Token': settings.options.accessToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ class PushoverAgent
|
|||||||
return settings.notifications.agents.pushover;
|
return settings.notifications.agents.pushover;
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.getSettings().enabled &&
|
settings.enabled &&
|
||||||
this.getSettings().options.accessToken &&
|
settings.options.accessToken &&
|
||||||
this.getSettings().options.userToken &&
|
settings.options.userToken
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -161,6 +162,12 @@ class PushoverAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Sending Pushover notification', {
|
logger.debug('Sending Pushover notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
@@ -169,14 +176,12 @@ class PushoverAgent
|
|||||||
try {
|
try {
|
||||||
const endpoint = 'https://api.pushover.net/1/messages.json';
|
const endpoint = 'https://api.pushover.net/1/messages.json';
|
||||||
|
|
||||||
const { accessToken, userToken } = this.getSettings().options;
|
|
||||||
|
|
||||||
const { title, message, url, url_title, priority } =
|
const { title, message, url, url_title, priority } =
|
||||||
this.constructMessageDetails(type, payload);
|
this.constructMessageDetails(type, payload);
|
||||||
|
|
||||||
await axios.post(endpoint, {
|
await axios.post(endpoint, {
|
||||||
token: accessToken,
|
token: settings.options.accessToken,
|
||||||
user: userToken,
|
user: settings.options.userToken,
|
||||||
title: title,
|
title: title,
|
||||||
message: message,
|
message: message,
|
||||||
url: url,
|
url: url,
|
||||||
|
|||||||
@@ -218,12 +218,10 @@ class SlackAgent
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
if (
|
const settings = this.getSettings();
|
||||||
this.getSettings().enabled &&
|
|
||||||
this.getSettings().options.webhookUrl &&
|
if (settings.enabled && settings.options.webhookUrl) {
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,19 +232,22 @@ class SlackAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Sending Slack notification', {
|
logger.debug('Sending Slack notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const webhookUrl = this.getSettings().options.webhookUrl;
|
await axios.post(
|
||||||
|
settings.options.webhookUrl,
|
||||||
if (!webhookUrl) {
|
this.buildEmbed(type, payload)
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
await axios.post(webhookUrl, this.buildEmbed(type, payload));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { getRepository } from 'typeorm';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
import { MediaType } from '../../../constants/media';
|
import { MediaType } from '../../../constants/media';
|
||||||
|
import { User } from '../../../entity/User';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
|
import { Permission } from '../../permissions';
|
||||||
import {
|
import {
|
||||||
getSettings,
|
getSettings,
|
||||||
NotificationAgentKey,
|
NotificationAgentKey,
|
||||||
@@ -40,12 +43,13 @@ class TelegramAgent
|
|||||||
return settings.notifications.agents.telegram;
|
return settings.notifications.agents.telegram;
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.getSettings().enabled &&
|
settings.enabled &&
|
||||||
this.getSettings().options.botAPI &&
|
settings.options.botAPI &&
|
||||||
this.getSettings().options.chatId &&
|
settings.options.chatId
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -59,8 +63,10 @@ class TelegramAgent
|
|||||||
|
|
||||||
private buildMessage(
|
private buildMessage(
|
||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload,
|
||||||
): string {
|
chatId: string,
|
||||||
|
sendSilently: boolean
|
||||||
|
): TelegramMessagePayload | TelegramPhotoPayload {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
let message = '';
|
let message = '';
|
||||||
|
|
||||||
@@ -153,95 +159,53 @@ class TelegramAgent
|
|||||||
}
|
}
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
return message;
|
return payload.image
|
||||||
|
? ({
|
||||||
|
photo: payload.image,
|
||||||
|
caption: message,
|
||||||
|
parse_mode: 'MarkdownV2',
|
||||||
|
chat_id: chatId,
|
||||||
|
disable_notification: !!sendSilently,
|
||||||
|
} as TelegramPhotoPayload)
|
||||||
|
: ({
|
||||||
|
text: message,
|
||||||
|
parse_mode: 'MarkdownV2',
|
||||||
|
chat_id: chatId,
|
||||||
|
disable_notification: !!sendSilently,
|
||||||
|
} as TelegramMessagePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(
|
public async send(
|
||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const endpoint = `${this.baseUrl}bot${this.getSettings().options.botAPI}/${
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${
|
||||||
payload.image ? 'sendPhoto' : 'sendMessage'
|
payload.image ? 'sendPhoto' : 'sendMessage'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
// Send system notification
|
// Send system notification
|
||||||
try {
|
if (hasNotificationType(type, settings.types ?? 0)) {
|
||||||
logger.debug('Sending Telegram notification', {
|
logger.debug('Sending Telegram notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
});
|
});
|
||||||
|
|
||||||
await axios.post(
|
|
||||||
endpoint,
|
|
||||||
payload.image
|
|
||||||
? ({
|
|
||||||
photo: payload.image,
|
|
||||||
caption: this.buildMessage(type, payload),
|
|
||||||
parse_mode: 'MarkdownV2',
|
|
||||||
chat_id: this.getSettings().options.chatId,
|
|
||||||
disable_notification: this.getSettings().options.sendSilently,
|
|
||||||
} as TelegramPhotoPayload)
|
|
||||||
: ({
|
|
||||||
text: this.buildMessage(type, payload),
|
|
||||||
parse_mode: 'MarkdownV2',
|
|
||||||
chat_id: `${this.getSettings().options.chatId}`,
|
|
||||||
disable_notification: this.getSettings().options.sendSilently,
|
|
||||||
} as TelegramMessagePayload)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Error sending Telegram notification', {
|
|
||||||
label: 'Notifications',
|
|
||||||
type: Notification[type],
|
|
||||||
subject: payload.subject,
|
|
||||||
errorMessage: e.message,
|
|
||||||
response: e.response.data,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload.notifyUser &&
|
|
||||||
payload.notifyUser.settings?.hasNotificationType(
|
|
||||||
NotificationAgentKey.TELEGRAM,
|
|
||||||
type
|
|
||||||
) &&
|
|
||||||
payload.notifyUser.settings?.telegramChatId &&
|
|
||||||
payload.notifyUser.settings?.telegramChatId !==
|
|
||||||
this.getSettings().options.chatId
|
|
||||||
) {
|
|
||||||
// Send notification to the user who submitted the request
|
|
||||||
logger.debug('Sending Telegram notification', {
|
|
||||||
label: 'Notifications',
|
|
||||||
recipient: payload.notifyUser.displayName,
|
|
||||||
type: Notification[type],
|
|
||||||
subject: payload.subject,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
endpoint,
|
endpoint,
|
||||||
payload.image
|
this.buildMessage(
|
||||||
? ({
|
type,
|
||||||
photo: payload.image,
|
payload,
|
||||||
caption: this.buildMessage(type, payload),
|
settings.options.chatId,
|
||||||
parse_mode: 'MarkdownV2',
|
settings.options.sendSilently
|
||||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
)
|
||||||
disable_notification:
|
|
||||||
payload.notifyUser.settings.telegramSendSilently,
|
|
||||||
} as TelegramPhotoPayload)
|
|
||||||
: ({
|
|
||||||
text: this.buildMessage(type, payload),
|
|
||||||
parse_mode: 'MarkdownV2',
|
|
||||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
|
||||||
disable_notification:
|
|
||||||
payload.notifyUser.settings.telegramSendSilently,
|
|
||||||
} as TelegramMessagePayload)
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending Telegram notification', {
|
logger.error('Error sending Telegram notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: payload.notifyUser.displayName,
|
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
@@ -252,6 +216,103 @@ class TelegramAgent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.notifyUser) {
|
||||||
|
// Send notification to the user who submitted the request
|
||||||
|
if (
|
||||||
|
payload.notifyUser.settings?.hasNotificationType(
|
||||||
|
NotificationAgentKey.TELEGRAM,
|
||||||
|
type
|
||||||
|
) &&
|
||||||
|
payload.notifyUser.settings?.telegramChatId &&
|
||||||
|
payload.notifyUser.settings?.telegramChatId !== settings.options.chatId
|
||||||
|
) {
|
||||||
|
logger.debug('Sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: payload.notifyUser.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
endpoint,
|
||||||
|
this.buildMessage(
|
||||||
|
type,
|
||||||
|
payload,
|
||||||
|
payload.notifyUser.settings.telegramChatId,
|
||||||
|
!!payload.notifyUser.settings.telegramSendSilently
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: payload.notifyUser.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response?.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Send notifications to all users with the Manage Requests permission
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const users = await userRepository.find();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
users
|
||||||
|
.filter(
|
||||||
|
(user) =>
|
||||||
|
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||||
|
user.settings?.hasNotificationType(
|
||||||
|
NotificationAgentKey.TELEGRAM,
|
||||||
|
type
|
||||||
|
) &&
|
||||||
|
// Check if it's the user's own auto-approved request
|
||||||
|
(type !== Notification.MEDIA_AUTO_APPROVED ||
|
||||||
|
user.id !== payload.request?.requestedBy.id)
|
||||||
|
)
|
||||||
|
.map(async (user) => {
|
||||||
|
if (
|
||||||
|
user.settings?.telegramChatId &&
|
||||||
|
user.settings.telegramChatId !== settings.options.chatId
|
||||||
|
) {
|
||||||
|
logger.debug('Sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
endpoint,
|
||||||
|
this.buildMessage(
|
||||||
|
type,
|
||||||
|
payload,
|
||||||
|
user.settings.telegramChatId,
|
||||||
|
!!user.settings?.telegramSendSilently
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response?.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,12 +113,10 @@ class WebhookAgent
|
|||||||
return this.parseKeys(parsedJSON, payload, type);
|
return this.parseKeys(parsedJSON, payload, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
if (
|
const settings = this.getSettings();
|
||||||
this.getSettings().enabled &&
|
|
||||||
this.getSettings().options.webhookUrl &&
|
if (settings.enabled && settings.options.webhookUrl) {
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +127,12 @@ class WebhookAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
|
||||||
|
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Sending webhook notification', {
|
logger.debug('Sending webhook notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
@@ -136,17 +140,17 @@ class WebhookAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webhookUrl, authHeader } = this.getSettings().options;
|
await axios.post(
|
||||||
|
settings.options.webhookUrl,
|
||||||
if (!webhookUrl) {
|
this.buildPayload(type, payload),
|
||||||
return false;
|
settings.options.authHeader
|
||||||
}
|
? {
|
||||||
|
headers: {
|
||||||
await axios.post(webhookUrl, this.buildPayload(type, payload), {
|
Authorization: settings.options.authHeader,
|
||||||
headers: {
|
},
|
||||||
Authorization: authHeader,
|
}
|
||||||
},
|
: undefined
|
||||||
});
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { Notification } from '..';
|
||||||
import { MediaType } from '../../../constants/media';
|
import { MediaType } from '../../../constants/media';
|
||||||
import { User } from '../../../entity/User';
|
import { User } from '../../../entity/User';
|
||||||
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
|
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
|
||||||
@@ -135,11 +135,8 @@ class WebPushAgent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(type: Notification): boolean {
|
public shouldSend(): boolean {
|
||||||
if (
|
if (this.getSettings().enabled) {
|
||||||
this.getSettings().enabled &&
|
|
||||||
hasNotificationType(type, this.getSettings().types)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +147,6 @@ class WebPushAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending web push notification', {
|
|
||||||
label: 'Notifications',
|
|
||||||
type: Notification[type],
|
|
||||||
subject: payload.subject,
|
|
||||||
});
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
@@ -213,8 +205,15 @@ class WebPushAgent
|
|||||||
settings.vapidPrivate
|
settings.vapidPrivate
|
||||||
);
|
);
|
||||||
|
|
||||||
Promise.all(
|
await Promise.all(
|
||||||
pushSubs.map(async (sub) => {
|
pushSubs.map(async (sub) => {
|
||||||
|
logger.debug('Sending web push notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: sub.user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await webpush.sendNotification(
|
await webpush.sendNotification(
|
||||||
{
|
{
|
||||||
@@ -230,12 +229,24 @@ class WebPushAgent
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} 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
|
// Failed to send notification so we need to remove the subscription
|
||||||
userPushSubRepository.remove(sub);
|
userPushSubRepository.remove(sub);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ export const hasNotificationType = (
|
|||||||
total = types;
|
total = types;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test notifications don't need to be enabled
|
||||||
|
if (!(value & Notification.TEST_NOTIFICATION)) {
|
||||||
|
value += Notification.TEST_NOTIFICATION;
|
||||||
|
}
|
||||||
|
|
||||||
return !!(value & total);
|
return !!(value & total);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +56,7 @@ class NotificationManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.activeAgents.forEach((agent) => {
|
this.activeAgents.forEach((agent) => {
|
||||||
if (agent.shouldSend(type)) {
|
if (agent.shouldSend()) {
|
||||||
agent.send(type, payload);
|
agent.send(type, payload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ interface FullPublicSettings extends PublicSettings {
|
|||||||
|
|
||||||
export interface NotificationAgentConfig {
|
export interface NotificationAgentConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
types: number;
|
types?: number;
|
||||||
options: Record<string, unknown>;
|
options: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
export interface NotificationAgentDiscord extends NotificationAgentConfig {
|
export interface NotificationAgentDiscord extends NotificationAgentConfig {
|
||||||
@@ -150,7 +150,7 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
|
|||||||
export interface NotificationAgentLunaSea extends NotificationAgentConfig {
|
export interface NotificationAgentLunaSea extends NotificationAgentConfig {
|
||||||
options: {
|
options: {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
profileName: string;
|
profileName?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,6 @@ export interface NotificationAgentPushover extends NotificationAgentConfig {
|
|||||||
options: {
|
options: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
userToken: string;
|
userToken: string;
|
||||||
priority: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
|
|||||||
options: {
|
options: {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
jsonPayload: string;
|
jsonPayload: string;
|
||||||
authHeader: string;
|
authHeader?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +271,6 @@ class Settings {
|
|||||||
agents: {
|
agents: {
|
||||||
email: {
|
email: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
types: 0,
|
|
||||||
options: {
|
options: {
|
||||||
emailFrom: '',
|
emailFrom: '',
|
||||||
smtpHost: '',
|
smtpHost: '',
|
||||||
@@ -288,8 +286,6 @@ class Settings {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
types: 0,
|
types: 0,
|
||||||
options: {
|
options: {
|
||||||
botUsername: '',
|
|
||||||
botAvatarUrl: '',
|
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -298,7 +294,6 @@ class Settings {
|
|||||||
types: 0,
|
types: 0,
|
||||||
options: {
|
options: {
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
profileName: '',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
slack: {
|
slack: {
|
||||||
@@ -312,7 +307,6 @@ class Settings {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
types: 0,
|
types: 0,
|
||||||
options: {
|
options: {
|
||||||
botUsername: '',
|
|
||||||
botAPI: '',
|
botAPI: '',
|
||||||
chatId: '',
|
chatId: '',
|
||||||
sendSilently: false,
|
sendSilently: false,
|
||||||
@@ -331,7 +325,6 @@ class Settings {
|
|||||||
options: {
|
options: {
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
userToken: '',
|
userToken: '',
|
||||||
priority: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
webhook: {
|
webhook: {
|
||||||
@@ -339,14 +332,12 @@ class Settings {
|
|||||||
types: 0,
|
types: 0,
|
||||||
options: {
|
options: {
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
authHeader: '',
|
|
||||||
jsonPayload:
|
jsonPayload:
|
||||||
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
|
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
webpush: {
|
webpush: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
types: 0,
|
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
|||||||
isOwnProfileOrAdmin(),
|
isOwnProfileOrAdmin(),
|
||||||
async (req, res, next) => {
|
async (req, res, next) => {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const settings = getSettings();
|
const settings = getSettings()?.notifications.agents;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await userRepository.findOne({
|
const user = await userRepository.findOne({
|
||||||
@@ -250,16 +250,18 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
emailEnabled: settings?.notifications.agents.email.enabled,
|
emailEnabled: settings?.email.enabled,
|
||||||
pgpKey: user.settings?.pgpKey,
|
pgpKey: user.settings?.pgpKey,
|
||||||
discordEnabled: settings?.notifications.agents.discord.enabled,
|
discordEnabled: settings?.discord.enabled,
|
||||||
|
discordEnabledTypes: settings?.discord.enabled
|
||||||
|
? settings?.discord.types
|
||||||
|
: 0,
|
||||||
discordId: user.settings?.discordId,
|
discordId: user.settings?.discordId,
|
||||||
telegramEnabled: settings?.notifications.agents.telegram.enabled,
|
telegramEnabled: settings?.telegram.enabled,
|
||||||
telegramBotUsername:
|
telegramBotUsername: settings?.telegram.options.botUsername,
|
||||||
settings?.notifications.agents.telegram.options.botUsername,
|
|
||||||
telegramChatId: user.settings?.telegramChatId,
|
telegramChatId: user.settings?.telegramChatId,
|
||||||
telegramSendSilently: user?.settings?.telegramSendSilently,
|
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||||
webPushEnabled: settings?.notifications.agents.webpush.enabled,
|
webPushEnabled: settings?.webpush.enabled,
|
||||||
notificationTypes: user.settings?.notificationTypes ?? {},
|
notificationTypes: user.settings?.notificationTypes ?? {},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path d="m81.9 83.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1 0.1-6.1-4.5-11.1-10.2-11.1zm36.5 0c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z" fill="currentColor"/><path d="m167 0h-134c-11.3 0-20.5 9.2-20.5 20.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19v-179.4c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-0.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-0.9-4.8-2-7.3-3.4-0.3-0.2-0.6-0.3-0.9-0.5-0.2-0.1-0.3-0.2-0.4-0.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-0.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3 0.6-0.1 1.1-0.2 1.7-0.2 6.1-0.8 13-1 20.2-0.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-0.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z" fill="currentColor"/></svg>
|
<svg viewBox="0 0 71 71" xmlns="http://www.w3.org/2000/svg"><path transform="translate(-1.0994e-7 8.0294)" d="m60.104 4.8978c-4.5253-2.0764-9.378-3.6062-14.452-4.4824-0.0924-0.01691-0.1847 0.025349-0.2323 0.10987-0.6241 1.11-1.3154 2.5581-1.7995 3.6963-5.4572-0.817-10.886-0.817-16.232 0-0.4842-1.1635-1.2006-2.5863-1.8275-3.6963-0.0476-0.0817-0.1399-0.12396-0.2323-0.10987-5.071 0.87338-9.9237 2.4032-14.452 4.4824-0.0392 0.0169-0.0728 0.0451-0.0951 0.0817-9.2046 13.751-11.726 27.165-10.489 40.412 0.005597 0.0648 0.041978 0.1268 0.092353 0.1662 6.0729 4.4598 11.956 7.1673 17.729 8.9619 0.0924 0.0282 0.1903-0.0056 0.2491-0.0817 1.3657-1.865 2.5831-3.8315 3.6269-5.8995 0.0616-0.1211 0.0028-0.2648-0.1231-0.3127-1.931-0.7325-3.7697-1.6256-5.5384-2.6398-0.1399-0.0817-0.1511-0.2818-0.0224-0.3776 0.3722-0.2789 0.7445-0.5691 1.0999-0.8621 0.0643-0.0535 0.1539-0.0648 0.2295-0.031 11.62 5.3051 24.199 5.3051 35.682 0 0.0756-0.0366 0.1652-0.0253 0.2323 0.0282 0.3555 0.293 0.7277 0.586 1.1027 0.8649 0.1287 0.0958 0.1203 0.2959-0.0196 0.3776-1.7687 1.0339-3.6074 1.9073-5.5412 2.637-0.1259 0.0479-0.1819 0.1944-0.1203 0.3155 1.0662 2.0651 2.2836 4.0316 3.6241 5.8967 0.056 0.0789 0.1567 0.1127 0.2491 0.0845 5.8014-1.7946 11.684-4.5021 17.757-8.9619 0.0532-0.0394 0.0868-0.0986 0.0924-0.1634 1.4804-15.315-2.4796-28.618-10.498-40.412-0.0196-0.0394-0.0531-0.0676-0.0923-0.0845zm-36.379 32.428c-3.4983 0-6.3808-3.2117-6.3808-7.156s2.8266-7.156 6.3808-7.156c3.5821 0 6.4367 3.2399 6.3807 7.156 0 3.9443-2.8266 7.156-6.3807 7.156zm23.592 0c-3.4982 0-6.3807-3.2117-6.3807-7.156s2.8265-7.156 6.3807-7.156c3.5822 0 6.4367 3.2399 6.3808 7.156 0 3.9443-2.7986 7.156-6.3808 7.156z" fill="currentColor"/></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -38,7 +38,7 @@ const NotificationType: React.FC<NotificationTypeProps> = ({
|
|||||||
: currentTypes + option.value
|
: currentTypes + option.value
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
defaultChecked={
|
checked={
|
||||||
hasNotificationType(option.value, currentTypes) ||
|
hasNotificationType(option.value, currentTypes) ||
|
||||||
(!!parent?.value &&
|
(!!parent?.value &&
|
||||||
hasNotificationType(parent.value, currentTypes))
|
hasNotificationType(parent.value, currentTypes))
|
||||||
@@ -46,10 +46,12 @@ const NotificationType: React.FC<NotificationTypeProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-sm leading-6">
|
<div className="ml-3 text-sm leading-6">
|
||||||
<label htmlFor={option.id} className="font-medium text-white">
|
<label htmlFor={option.id} className="block font-medium text-white">
|
||||||
{option.name}
|
<div className="flex flex-col">
|
||||||
|
<span>{option.name}</span>
|
||||||
|
<span className="text-gray-500">{option.description}</span>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-500">{option.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(option.children ?? []).map((child) => (
|
{(option.children ?? []).map((child) => (
|
||||||
|
|||||||
@@ -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 { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSettings from '../../hooks/useSettings';
|
||||||
|
import { Permission, User, useUser } from '../../hooks/useUser';
|
||||||
import NotificationType from './NotificationType';
|
import NotificationType from './NotificationType';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
notificationTypes: 'Notification Types',
|
notificationTypes: 'Notification Types',
|
||||||
mediarequested: 'Media Requested',
|
mediarequested: 'Media Requested',
|
||||||
mediarequestedDescription:
|
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',
|
mediaapproved: 'Media Approved',
|
||||||
mediaapprovedDescription:
|
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',
|
mediaAutoApproved: 'Media Automatically Approved',
|
||||||
mediaAutoApprovedDescription:
|
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',
|
mediaavailable: 'Media Available',
|
||||||
mediaavailableDescription:
|
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',
|
mediafailed: 'Media Failed',
|
||||||
mediafailedDescription:
|
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',
|
mediadeclined: 'Media Declined',
|
||||||
mediadeclinedDescription:
|
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 = (
|
export const hasNotificationType = (
|
||||||
@@ -30,16 +45,23 @@ export const hasNotificationType = (
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
|
// If we are not checking any notifications, bail out and return true
|
||||||
if (types === 0) {
|
if (types === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(types)) {
|
if (Array.isArray(types)) {
|
||||||
|
// Combine all notification values into one
|
||||||
total = types.reduce((a, v) => a + v, 0);
|
total = types.reduce((a, v) => a + v, 0);
|
||||||
} else {
|
} else {
|
||||||
total = types;
|
total = types;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test notifications don't need to be enabled
|
||||||
|
if (!(value & Notification.TEST_NOTIFICATION)) {
|
||||||
|
value += Notification.TEST_NOTIFICATION;
|
||||||
|
}
|
||||||
|
|
||||||
return !!(value & total);
|
return !!(value & total);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,69 +85,183 @@ export interface NotificationItem {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
value: Notification;
|
value: Notification;
|
||||||
|
hasNotifyUser?: boolean;
|
||||||
children?: NotificationItem[];
|
children?: NotificationItem[];
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationTypeSelectorProps {
|
interface NotificationTypeSelectorProps {
|
||||||
|
user?: User;
|
||||||
|
enabledTypes?: number;
|
||||||
currentTypes: number;
|
currentTypes: number;
|
||||||
onUpdate: (newTypes: number) => void;
|
onUpdate: (newTypes: number) => void;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
||||||
|
user,
|
||||||
|
enabledTypes = ALL_NOTIFICATIONS,
|
||||||
currentTypes,
|
currentTypes,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const settings = useSettings();
|
||||||
|
const { hasPermission } = useUser({ id: user?.id });
|
||||||
|
const [allowedTypes, setAllowedTypes] = useState(enabledTypes);
|
||||||
|
|
||||||
const types: NotificationItem[] = [
|
const availableTypes = useMemo(() => {
|
||||||
{
|
const allRequestsAutoApproved =
|
||||||
id: 'media-requested',
|
user &&
|
||||||
name: intl.formatMessage(messages.mediarequested),
|
// Has Manage Requests perm, which grants all Auto-Approve perms
|
||||||
description: intl.formatMessage(messages.mediarequestedDescription),
|
(hasPermission(Permission.MANAGE_REQUESTS) ||
|
||||||
value: Notification.MEDIA_PENDING,
|
// Cannot submit requests of any type
|
||||||
},
|
!hasPermission(
|
||||||
{
|
[
|
||||||
id: 'media-auto-approved',
|
Permission.REQUEST,
|
||||||
name: intl.formatMessage(messages.mediaAutoApproved),
|
Permission.REQUEST_MOVIE,
|
||||||
description: intl.formatMessage(messages.mediaAutoApprovedDescription),
|
Permission.REQUEST_TV,
|
||||||
value: Notification.MEDIA_AUTO_APPROVED,
|
Permission.REQUEST_4K,
|
||||||
},
|
Permission.REQUEST_4K_MOVIE,
|
||||||
{
|
Permission.REQUEST_4K_TV,
|
||||||
id: 'media-approved',
|
],
|
||||||
name: intl.formatMessage(messages.mediaapproved),
|
{ type: 'or' }
|
||||||
description: intl.formatMessage(messages.mediaapprovedDescription),
|
) ||
|
||||||
value: Notification.MEDIA_APPROVED,
|
// Cannot submit non-4K movie requests OR has Auto-Approve perms for non-4K movies
|
||||||
},
|
((!hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], {
|
||||||
{
|
type: 'or',
|
||||||
id: 'media-declined',
|
}) ||
|
||||||
name: intl.formatMessage(messages.mediadeclined),
|
hasPermission(
|
||||||
description: intl.formatMessage(messages.mediadeclinedDescription),
|
[Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_MOVIE],
|
||||||
value: Notification.MEDIA_DECLINED,
|
{ type: 'or' }
|
||||||
},
|
)) &&
|
||||||
{
|
// Cannot submit non-4K series requests OR has Auto-Approve perms for non-4K series
|
||||||
id: 'media-available',
|
(!hasPermission([Permission.REQUEST, Permission.REQUEST_TV], {
|
||||||
name: intl.formatMessage(messages.mediaavailable),
|
type: 'or',
|
||||||
description: intl.formatMessage(messages.mediaavailableDescription),
|
}) ||
|
||||||
value: Notification.MEDIA_AVAILABLE,
|
hasPermission(
|
||||||
},
|
[Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_TV],
|
||||||
{
|
{ type: 'or' }
|
||||||
id: 'media-failed',
|
)) &&
|
||||||
name: intl.formatMessage(messages.mediafailed),
|
// Cannot submit 4K movie requests OR has Auto-Approve perms for 4K movies
|
||||||
description: intl.formatMessage(messages.mediafailedDescription),
|
(!settings.currentSettings.movie4kEnabled ||
|
||||||
value: Notification.MEDIA_FAILED,
|
!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 (
|
return (
|
||||||
<div role="group" aria-labelledby="group-label" className="form-group">
|
<div role="group" aria-labelledby="group-label" className="form-group">
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<span id="group-label" className="group-label">
|
<span id="group-label" className="group-label">
|
||||||
{intl.formatMessage(messages.notificationTypes)}
|
{intl.formatMessage(messages.notificationTypes)}
|
||||||
<span className="label-required">*</span>
|
{!user && <span className="label-required">*</span>}
|
||||||
</span>
|
</span>
|
||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="max-w-lg">
|
<div className="max-w-lg">
|
||||||
{types.map((type) => (
|
{availableTypes.map((type) => (
|
||||||
<NotificationType
|
<NotificationType
|
||||||
key={`notification-type-${type.id}`}
|
key={`notification-type-${type.id}`}
|
||||||
option={type}
|
option={type}
|
||||||
@@ -134,6 +270,7 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
|||||||
toastDiscordTestSuccess: 'Discord test notification sent!',
|
toastDiscordTestSuccess: 'Discord test notification sent!',
|
||||||
toastDiscordTestFailed: 'Discord test notification failed to send.',
|
toastDiscordTestFailed: 'Discord test notification failed to send.',
|
||||||
validationUrl: 'You must provide a valid URL',
|
validationUrl: 'You must provide a valid URL',
|
||||||
|
validationTypes: 'You must select at least one notification type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsDiscord: React.FC = () => {
|
const NotificationsDiscord: React.FC = () => {
|
||||||
@@ -46,6 +47,13 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
otherwise: Yup.string().nullable(),
|
otherwise: Yup.string().nullable(),
|
||||||
})
|
})
|
||||||
.url(intl.formatMessage(messages.validationUrl)),
|
.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) {
|
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 () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -211,8 +227,20 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import { useToasts } from 'react-toast-notifications';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import globalMessages from '../../../i18n/globalMessages';
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
import Alert from '../../Common/Alert';
|
|
||||||
import Badge from '../../Common/Badge';
|
import Badge from '../../Common/Badge';
|
||||||
import Button from '../../Common/Button';
|
import Button from '../../Common/Button';
|
||||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
import SensitiveInput from '../../Common/SensitiveInput';
|
import SensitiveInput from '../../Common/SensitiveInput';
|
||||||
import NotificationTypeSelector from '../../NotificationTypeSelector';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
||||||
@@ -37,20 +35,14 @@ const messages = defineMessages({
|
|||||||
allowselfsigned: 'Allow Self-Signed Certificates',
|
allowselfsigned: 'Allow Self-Signed Certificates',
|
||||||
senderName: 'Sender Name',
|
senderName: 'Sender Name',
|
||||||
validationEmail: 'You must provide a valid email address',
|
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',
|
pgpPrivateKey: 'PGP Private Key',
|
||||||
pgpPrivateKeyTip:
|
pgpPrivateKeyTip:
|
||||||
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||||
validationPgpPrivateKey:
|
validationPgpPrivateKey: 'You must provide a valid PGP private key',
|
||||||
'You must provide a valid PGP private key if a PGP password is entered',
|
|
||||||
pgpPassword: 'PGP Password',
|
pgpPassword: 'PGP Password',
|
||||||
pgpPasswordTip:
|
pgpPasswordTip:
|
||||||
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||||
validationPgpPassword:
|
validationPgpPassword: 'You must provide a PGP password',
|
||||||
'You must provide a PGP password if a PGP private key is entered',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function OpenPgpLink(msg: string): JSX.Element {
|
export function OpenPgpLink(msg: string): JSX.Element {
|
||||||
@@ -130,7 +122,6 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
types: data.types,
|
|
||||||
emailFrom: data.options.emailFrom,
|
emailFrom: data.options.emailFrom,
|
||||||
smtpHost: data.options.smtpHost,
|
smtpHost: data.options.smtpHost,
|
||||||
smtpPort: data.options.smtpPort ?? 587,
|
smtpPort: data.options.smtpPort ?? 587,
|
||||||
@@ -153,7 +144,6 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/settings/notifications/email', {
|
await axios.post('/api/v1/settings/notifications/email', {
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
types: values.types,
|
|
||||||
options: {
|
options: {
|
||||||
emailFrom: values.emailFrom,
|
emailFrom: values.emailFrom,
|
||||||
smtpHost: values.smtpHost,
|
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 () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -201,7 +191,6 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
);
|
);
|
||||||
await axios.post('/api/v1/settings/notifications/email/test', {
|
await axios.post('/api/v1/settings/notifications/email/test', {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
types: values.types,
|
|
||||||
options: {
|
options: {
|
||||||
emailFrom: values.emailFrom,
|
emailFrom: values.emailFrom,
|
||||||
smtpHost: values.smtpHost,
|
smtpHost: values.smtpHost,
|
||||||
@@ -238,274 +227,234 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Form className="section">
|
||||||
<Alert
|
<div className="form-row">
|
||||||
title={
|
<label htmlFor="enabled" className="checkbox-label">
|
||||||
<>
|
{intl.formatMessage(messages.agentenabled)}
|
||||||
<p className="mb-2">
|
<span className="label-required">*</span>
|
||||||
{intl.formatMessage(
|
</label>
|
||||||
messages.emailNotificationTypesAlertDescription,
|
<div className="form-input">
|
||||||
{
|
<Field type="checkbox" id="enabled" name="enabled" />
|
||||||
strong: function strong(msg) {
|
</div>
|
||||||
return (
|
</div>
|
||||||
<strong className="font-semibold text-indigo-100">
|
<div className="form-row">
|
||||||
{msg}
|
<label htmlFor="senderName" className="text-label">
|
||||||
</strong>
|
{intl.formatMessage(messages.senderName)}
|
||||||
);
|
</label>
|
||||||
},
|
<div className="form-input">
|
||||||
}
|
<div className="form-input-field">
|
||||||
)}
|
<Field id="senderName" name="senderName" type="text" />
|
||||||
</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" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
</div>
|
||||||
<label htmlFor="senderName" className="text-label">
|
<div className="form-row">
|
||||||
{intl.formatMessage(messages.senderName)}
|
<label htmlFor="emailFrom" className="text-label">
|
||||||
</label>
|
{intl.formatMessage(messages.emailsender)}
|
||||||
<div className="form-input">
|
<span className="label-required">*</span>
|
||||||
<div className="form-input-field">
|
</label>
|
||||||
<Field id="senderName" name="senderName" type="text" />
|
<div className="form-input">
|
||||||
</div>
|
<div className="form-input-field">
|
||||||
</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">
|
|
||||||
<Field
|
<Field
|
||||||
id="smtpPort"
|
id="emailFrom"
|
||||||
name="smtpPort"
|
name="emailFrom"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="email"
|
||||||
className="short"
|
|
||||||
/>
|
/>
|
||||||
{errors.smtpPort && touched.smtpPort && (
|
|
||||||
<div className="error">{errors.smtpPort}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{errors.emailFrom && touched.emailFrom && (
|
||||||
|
<div className="error">{errors.emailFrom}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
</div>
|
||||||
<label htmlFor="encryption" className="text-label">
|
<div className="form-row">
|
||||||
{intl.formatMessage(messages.encryption)}
|
<label htmlFor="smtpHost" className="text-label">
|
||||||
<span className="label-required">*</span>
|
{intl.formatMessage(messages.smtpHost)}
|
||||||
</label>
|
<span className="label-required">*</span>
|
||||||
<div className="form-input">
|
</label>
|
||||||
<div className="form-input-field">
|
<div className="form-input">
|
||||||
<Field as="select" id="encryption" name="encryption">
|
<div className="form-input-field">
|
||||||
<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
|
<Field
|
||||||
type="checkbox"
|
id="smtpHost"
|
||||||
id="allowSelfSigned"
|
name="smtpHost"
|
||||||
name="allowSelfSigned"
|
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>
|
</div>
|
||||||
<div className="form-row">
|
</div>
|
||||||
<label htmlFor="authUser" className="text-label">
|
<div className="form-row">
|
||||||
{intl.formatMessage(messages.authUser)}
|
<label htmlFor="pgpPrivateKey" className="text-label">
|
||||||
</label>
|
<span className="mr-2">
|
||||||
<div className="form-input">
|
{intl.formatMessage(messages.pgpPrivateKey)}
|
||||||
<div className="form-input-field">
|
</span>
|
||||||
<Field id="authUser" name="authUser" type="text" />
|
<Badge badgeType="danger">
|
||||||
</div>
|
{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>
|
</div>
|
||||||
|
{errors.pgpPrivateKey && touched.pgpPrivateKey && (
|
||||||
|
<div className="error">{errors.pgpPrivateKey}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
</div>
|
||||||
<label htmlFor="authPass" className="text-label">
|
<div className="form-row">
|
||||||
{intl.formatMessage(messages.authPass)}
|
<label htmlFor="pgpPassword" className="text-label">
|
||||||
</label>
|
<span className="mr-2">
|
||||||
<div className="form-input">
|
{intl.formatMessage(messages.pgpPassword)}
|
||||||
<div className="form-input-field">
|
</span>
|
||||||
<SensitiveInput
|
<Badge badgeType="danger">
|
||||||
as="field"
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
id="authPass"
|
</Badge>
|
||||||
name="authPass"
|
<span className="label-tip">
|
||||||
autoComplete="one-time-code"
|
{intl.formatMessage(messages.pgpPasswordTip, {
|
||||||
/>
|
OpenPgpLink: OpenPgpLink,
|
||||||
</div>
|
})}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<SensitiveInput
|
||||||
|
as="field"
|
||||||
|
id="pgpPassword"
|
||||||
|
name="pgpPassword"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.pgpPassword && touched.pgpPassword && (
|
||||||
|
<div className="error">{errors.pgpPassword}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
</div>
|
||||||
<label htmlFor="pgpPrivateKey" className="text-label">
|
<div className="actions">
|
||||||
<span className="mr-2">
|
<div className="flex justify-end">
|
||||||
{intl.formatMessage(messages.pgpPrivateKey)}
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
</span>
|
<Button
|
||||||
<Badge badgeType="danger">
|
buttonType="warning"
|
||||||
{intl.formatMessage(globalMessages.advanced)}
|
disabled={isSubmitting || !isValid || isTesting}
|
||||||
</Badge>
|
onClick={(e) => {
|
||||||
<span className="label-tip">
|
e.preventDefault();
|
||||||
{intl.formatMessage(messages.pgpPrivateKeyTip, {
|
testSettings();
|
||||||
OpenPgpLink: OpenPgpLink,
|
}}
|
||||||
})}
|
>
|
||||||
</span>
|
{isTesting
|
||||||
</label>
|
? intl.formatMessage(globalMessages.testing)
|
||||||
<div className="form-input">
|
: intl.formatMessage(globalMessages.test)}
|
||||||
<div className="form-input-field">
|
</Button>
|
||||||
<SensitiveInput
|
</span>
|
||||||
as="field"
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
id="pgpPrivateKey"
|
<Button
|
||||||
name="pgpPrivateKey"
|
buttonType="primary"
|
||||||
type="textarea"
|
type="submit"
|
||||||
rows="10"
|
disabled={isSubmitting || !isValid || isTesting}
|
||||||
className="font-mono text-xs"
|
>
|
||||||
/>
|
{isSubmitting
|
||||||
</div>
|
? intl.formatMessage(globalMessages.saving)
|
||||||
{errors.pgpPrivateKey && touched.pgpPrivateKey && (
|
: intl.formatMessage(globalMessages.save)}
|
||||||
<div className="error">{errors.pgpPrivateKey}</div>
|
</Button>
|
||||||
)}
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
</div>
|
||||||
<label htmlFor="pgpPassword" className="text-label">
|
</Form>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
|||||||
toastLunaSeaTestSending: 'Sending LunaSea test notification…',
|
toastLunaSeaTestSending: 'Sending LunaSea test notification…',
|
||||||
toastLunaSeaTestSuccess: 'LunaSea test notification sent!',
|
toastLunaSeaTestSuccess: 'LunaSea test notification sent!',
|
||||||
toastLunaSeaTestFailed: 'LunaSea test notification failed to send.',
|
toastLunaSeaTestFailed: 'LunaSea test notification failed to send.',
|
||||||
|
validationTypes: 'You must select at least one notification type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsLunaSea: React.FC = () => {
|
const NotificationsLunaSea: React.FC = () => {
|
||||||
@@ -43,6 +44,13 @@ const NotificationsLunaSea: React.FC = () => {
|
|||||||
otherwise: Yup.string().nullable(),
|
otherwise: Yup.string().nullable(),
|
||||||
})
|
})
|
||||||
.url(intl.formatMessage(messages.validationWebhookUrl)),
|
.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) {
|
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 () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -190,8 +206,20 @@ const NotificationsLunaSea: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
|||||||
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
|
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
|
||||||
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
|
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
|
||||||
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
|
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
|
||||||
|
validationTypes: 'You must select at least one notification type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsPushbullet: React.FC = () => {
|
const NotificationsPushbullet: React.FC = () => {
|
||||||
@@ -41,6 +42,13 @@ const NotificationsPushbullet: React.FC = () => {
|
|||||||
.required(intl.formatMessage(messages.validationAccessTokenRequired)),
|
.required(intl.formatMessage(messages.validationAccessTokenRequired)),
|
||||||
otherwise: Yup.string().nullable(),
|
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) {
|
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 () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -170,8 +186,20 @@ const NotificationsPushbullet: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ const messages = defineMessages({
|
|||||||
userTokenTip:
|
userTokenTip:
|
||||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||||
validationAccessTokenRequired: 'You must provide a valid application token',
|
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!',
|
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||||
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
||||||
toastPushoverTestSending: 'Sending Pushover test notification…',
|
toastPushoverTestSending: 'Sending Pushover test notification…',
|
||||||
toastPushoverTestSuccess: 'Pushover test notification sent!',
|
toastPushoverTestSuccess: 'Pushover test notification sent!',
|
||||||
toastPushoverTestFailed: 'Pushover test notification failed to send.',
|
toastPushoverTestFailed: 'Pushover test notification failed to send.',
|
||||||
|
validationTypes: 'You must select at least one notification type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsPushover: React.FC = () => {
|
const NotificationsPushover: React.FC = () => {
|
||||||
@@ -60,6 +61,13 @@ const NotificationsPushover: React.FC = () => {
|
|||||||
/^[a-z\d]{30}$/i,
|
/^[a-z\d]{30}$/i,
|
||||||
intl.formatMessage(messages.validationUserTokenRequired)
|
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) {
|
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 () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -216,8 +232,20 @@ const NotificationsPushover: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const messages = defineMessages({
|
|||||||
toastSlackTestSuccess: 'Slack test notification sent!',
|
toastSlackTestSuccess: 'Slack test notification sent!',
|
||||||
toastSlackTestFailed: 'Slack test notification failed to send.',
|
toastSlackTestFailed: 'Slack test notification failed to send.',
|
||||||
validationWebhookUrl: 'You must provide a valid URL',
|
validationWebhookUrl: 'You must provide a valid URL',
|
||||||
|
validationTypes: 'You must select at least one notification type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsSlack: React.FC = () => {
|
const NotificationsSlack: React.FC = () => {
|
||||||
@@ -41,6 +42,13 @@ const NotificationsSlack: React.FC = () => {
|
|||||||
otherwise: Yup.string().nullable(),
|
otherwise: Yup.string().nullable(),
|
||||||
})
|
})
|
||||||
.url(intl.formatMessage(messages.validationWebhookUrl)),
|
.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) {
|
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 () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -168,8 +184,20 @@ const NotificationsSlack: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -105,7 +105,15 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
{({
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
isSubmitting,
|
||||||
|
values,
|
||||||
|
isValid,
|
||||||
|
setFieldValue,
|
||||||
|
setFieldTouched,
|
||||||
|
}) => {
|
||||||
const testSettings = async () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -232,6 +240,24 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
<label htmlFor="chatId" className="text-label">
|
<label htmlFor="chatId" className="text-label">
|
||||||
{intl.formatMessage(messages.chatId)}
|
{intl.formatMessage(messages.chatId)}
|
||||||
<span className="label-required">*</span>
|
<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>
|
</label>
|
||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
@@ -254,8 +280,20 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import globalMessages from '../../../../i18n/globalMessages';
|
|||||||
import Alert from '../../../Common/Alert';
|
import Alert from '../../../Common/Alert';
|
||||||
import Button from '../../../Common/Button';
|
import Button from '../../../Common/Button';
|
||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
agentenabled: 'Enable Agent',
|
agentenabled: 'Enable Agent',
|
||||||
@@ -49,13 +48,11 @@ const NotificationsWebPush: React.FC = () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
types: data.types,
|
|
||||||
}}
|
}}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/settings/notifications/webpush', {
|
await axios.post('/api/v1/settings/notifications/webpush', {
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
types: values.types,
|
|
||||||
options: {},
|
options: {},
|
||||||
});
|
});
|
||||||
mutate('/api/v1/settings/public');
|
mutate('/api/v1/settings/public');
|
||||||
@@ -73,7 +70,7 @@ const NotificationsWebPush: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, values, isValid, setFieldValue }) => {
|
{({ isSubmitting }) => {
|
||||||
const testSettings = async () => {
|
const testSettings = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
let toastId: string | undefined;
|
let toastId: string | undefined;
|
||||||
@@ -90,7 +87,6 @@ const NotificationsWebPush: React.FC = () => {
|
|||||||
);
|
);
|
||||||
await axios.post('/api/v1/settings/notifications/webpush/test', {
|
await axios.post('/api/v1/settings/notifications/webpush/test', {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
types: values.types,
|
|
||||||
options: {},
|
options: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,16 +121,12 @@ const NotificationsWebPush: React.FC = () => {
|
|||||||
<Field type="checkbox" id="enabled" name="enabled" />
|
<Field type="checkbox" id="enabled" name="enabled" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
|
||||||
currentTypes={values.types}
|
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
|
||||||
/>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
<Button
|
<Button
|
||||||
buttonType="warning"
|
buttonType="warning"
|
||||||
disabled={isSubmitting || !isValid || isTesting}
|
disabled={isSubmitting || isTesting}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
testSettings();
|
testSettings();
|
||||||
@@ -149,7 +141,7 @@ const NotificationsWebPush: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting || !isValid || isTesting}
|
disabled={isSubmitting || isTesting}
|
||||||
>
|
>
|
||||||
{isSubmitting
|
{isSubmitting
|
||||||
? intl.formatMessage(globalMessages.saving)
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const messages = defineMessages({
|
|||||||
customJson: 'JSON Payload',
|
customJson: 'JSON Payload',
|
||||||
templatevariablehelp: 'Template Variable Help',
|
templatevariablehelp: 'Template Variable Help',
|
||||||
validationWebhookUrl: 'You must provide a valid URL',
|
validationWebhookUrl: 'You must provide a valid URL',
|
||||||
|
validationTypes: 'You must select at least one notification type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsWebhook: React.FC = () => {
|
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) {
|
if (!data && !error) {
|
||||||
@@ -293,8 +301,20 @@ const NotificationsWebhook: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.enabled ? values.types : 0}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser';
|
|||||||
import globalMessages from '../../../../i18n/globalMessages';
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
import Button from '../../../Common/Button';
|
import Button from '../../../Common/Button';
|
||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
discordsettingssaved: 'Discord notification settings saved successfully!',
|
discordsettingssaved: 'Discord notification settings saved successfully!',
|
||||||
discordsettingsfailed: 'Discord notification settings failed to save.',
|
discordsettingsfailed: 'Discord notification settings failed to save.',
|
||||||
enableDiscord: 'Enable Mentions',
|
|
||||||
discordId: 'User ID',
|
discordId: 'User ID',
|
||||||
discordIdTip:
|
discordIdTip:
|
||||||
'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your user account',
|
'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your user account',
|
||||||
@@ -34,8 +33,8 @@ const UserNotificationsDiscord: React.FC = () => {
|
|||||||
|
|
||||||
const UserNotificationsDiscordSchema = Yup.object().shape({
|
const UserNotificationsDiscordSchema = Yup.object().shape({
|
||||||
discordId: Yup.string()
|
discordId: Yup.string()
|
||||||
.when('enableDiscord', {
|
.when('types', {
|
||||||
is: true,
|
is: (value: unknown) => !!value,
|
||||||
then: Yup.string()
|
then: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.required(intl.formatMessage(messages.validationDiscordId)),
|
.required(intl.formatMessage(messages.validationDiscordId)),
|
||||||
@@ -51,8 +50,10 @@ const UserNotificationsDiscord: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enableDiscord: !!data?.notificationTypes.discord,
|
|
||||||
discordId: data?.discordId,
|
discordId: data?.discordId,
|
||||||
|
types:
|
||||||
|
(data?.discordEnabledTypes ?? 0) &
|
||||||
|
(data?.notificationTypes.discord ?? 0),
|
||||||
}}
|
}}
|
||||||
validationSchema={UserNotificationsDiscordSchema}
|
validationSchema={UserNotificationsDiscordSchema}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
@@ -64,7 +65,7 @@ const UserNotificationsDiscord: React.FC = () => {
|
|||||||
telegramChatId: data?.telegramChatId,
|
telegramChatId: data?.telegramChatId,
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
notificationTypes: {
|
notificationTypes: {
|
||||||
discord: values.enableDiscord ? ALL_NOTIFICATIONS : 0,
|
discord: values.types,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
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 (
|
return (
|
||||||
<Form className="section">
|
<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">
|
<div className="form-row">
|
||||||
<label htmlFor="discordId" className="text-label">
|
<label htmlFor="discordId" className="text-label">
|
||||||
<span>{intl.formatMessage(messages.discordId)}</span>
|
{intl.formatMessage(messages.discordId)}
|
||||||
<span className="label-required">*</span>
|
{!!data?.discordEnabledTypes && (
|
||||||
|
<span className="label-required">*</span>
|
||||||
|
)}
|
||||||
<span className="label-tip">
|
<span className="label-tip">
|
||||||
{intl.formatMessage(messages.discordIdTip, {
|
{intl.formatMessage(messages.discordIdTip, {
|
||||||
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
||||||
@@ -127,6 +124,20 @@ const UserNotificationsDiscord: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Form, Formik } from 'formik';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
@@ -12,13 +12,15 @@ import globalMessages from '../../../../i18n/globalMessages';
|
|||||||
import Badge from '../../../Common/Badge';
|
import Badge from '../../../Common/Badge';
|
||||||
import Button from '../../../Common/Button';
|
import Button from '../../../Common/Button';
|
||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
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';
|
import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emailsettingssaved: 'Email notification settings saved successfully!',
|
emailsettingssaved: 'Email notification settings saved successfully!',
|
||||||
emailsettingsfailed: 'Email notification settings failed to save.',
|
emailsettingsfailed: 'Email notification settings failed to save.',
|
||||||
enableEmail: 'Enable Notifications',
|
|
||||||
pgpPublicKey: 'PGP Public Key',
|
pgpPublicKey: 'PGP Public Key',
|
||||||
pgpPublicKeyTip:
|
pgpPublicKeyTip:
|
||||||
'Encrypt email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
'Encrypt email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||||
@@ -50,8 +52,8 @@ const UserEmailSettings: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enableEmail: !!(data?.notificationTypes.email ?? true),
|
|
||||||
pgpKey: data?.pgpKey,
|
pgpKey: data?.pgpKey,
|
||||||
|
types: data?.notificationTypes.email ?? ALL_NOTIFICATIONS,
|
||||||
}}
|
}}
|
||||||
validationSchema={UserNotificationsEmailSchema}
|
validationSchema={UserNotificationsEmailSchema}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
@@ -63,7 +65,7 @@ const UserEmailSettings: React.FC = () => {
|
|||||||
telegramChatId: data?.telegramChatId,
|
telegramChatId: data?.telegramChatId,
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
notificationTypes: {
|
notificationTypes: {
|
||||||
email: values.enableEmail ? ALL_NOTIFICATIONS : 0,
|
email: values.types,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
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 (
|
return (
|
||||||
<Form className="section">
|
<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">
|
<div className="form-row">
|
||||||
<label htmlFor="pgpKey" className="text-label">
|
<label htmlFor="pgpKey" className="text-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
@@ -107,8 +109,9 @@ const UserEmailSettings: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
<Field
|
<SensitiveInput
|
||||||
as="textarea"
|
as="field"
|
||||||
|
type="textarea"
|
||||||
id="pgpKey"
|
id="pgpKey"
|
||||||
name="pgpKey"
|
name="pgpKey"
|
||||||
rows="10"
|
rows="10"
|
||||||
@@ -120,6 +123,19 @@ const UserEmailSettings: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser';
|
|||||||
import globalMessages from '../../../../i18n/globalMessages';
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
import Button from '../../../Common/Button';
|
import Button from '../../../Common/Button';
|
||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
||||||
telegramsettingsfailed: 'Telegram notification settings failed to save.',
|
telegramsettingsfailed: 'Telegram notification settings failed to save.',
|
||||||
enableTelegram: 'Enable Notifications',
|
|
||||||
telegramChatId: 'Chat ID',
|
telegramChatId: 'Chat ID',
|
||||||
telegramChatIdTipLong:
|
telegramChatIdTipLong:
|
||||||
'<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
'<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({
|
const UserNotificationsTelegramSchema = Yup.object().shape({
|
||||||
telegramChatId: Yup.string()
|
telegramChatId: Yup.string()
|
||||||
.when('enableTelegram', {
|
.when('types', {
|
||||||
is: true,
|
is: (value: unknown) => !!value,
|
||||||
then: Yup.string()
|
then: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.required(intl.formatMessage(messages.validationTelegramChatId)),
|
.required(intl.formatMessage(messages.validationTelegramChatId)),
|
||||||
@@ -56,9 +55,9 @@ const UserTelegramSettings: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enableTelegram: !!data?.notificationTypes.telegram,
|
|
||||||
telegramChatId: data?.telegramChatId,
|
telegramChatId: data?.telegramChatId,
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
|
types: data?.notificationTypes.telegram ?? 0,
|
||||||
}}
|
}}
|
||||||
validationSchema={UserNotificationsTelegramSchema}
|
validationSchema={UserNotificationsTelegramSchema}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
@@ -70,7 +69,7 @@ const UserTelegramSettings: React.FC = () => {
|
|||||||
telegramChatId: values.telegramChatId,
|
telegramChatId: values.telegramChatId,
|
||||||
telegramSendSilently: values.telegramSendSilently,
|
telegramSendSilently: values.telegramSendSilently,
|
||||||
notificationTypes: {
|
notificationTypes: {
|
||||||
telegram: values.enableTelegram ? ALL_NOTIFICATIONS : 0,
|
telegram: values.types,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
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 (
|
return (
|
||||||
<Form className="section">
|
<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">
|
<div className="form-row">
|
||||||
<label htmlFor="telegramChatId" className="text-label">
|
<label htmlFor="telegramChatId" className="text-label">
|
||||||
{intl.formatMessage(messages.telegramChatId)}
|
{intl.formatMessage(messages.telegramChatId)}
|
||||||
@@ -166,6 +161,19 @@ const UserTelegramSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Form, Formik } from 'formik';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
@@ -10,12 +10,13 @@ import { useUser } from '../../../../hooks/useUser';
|
|||||||
import globalMessages from '../../../../i18n/globalMessages';
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
import Button from '../../../Common/Button';
|
import Button from '../../../Common/Button';
|
||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
import NotificationTypeSelector, {
|
||||||
|
ALL_NOTIFICATIONS,
|
||||||
|
} from '../../../NotificationTypeSelector';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
||||||
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
||||||
enableWebPush: 'Enable Notifications',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserWebPushSettings: React.FC = () => {
|
const UserWebPushSettings: React.FC = () => {
|
||||||
@@ -34,18 +35,18 @@ const UserWebPushSettings: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enableWebPush: !!(data?.notificationTypes.webpush ?? true),
|
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
|
||||||
pgpKey: data?.pgpKey,
|
|
||||||
}}
|
}}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||||
|
pgpKey: data?.pgpKey,
|
||||||
discordId: data?.discordId,
|
discordId: data?.discordId,
|
||||||
telegramChatId: data?.telegramChatId,
|
telegramChatId: data?.telegramChatId,
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
notificationTypes: {
|
notificationTypes: {
|
||||||
webpush: values.enableWebPush ? ALL_NOTIFICATIONS : 0,
|
webpush: values.types,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mutate('/api/v1/settings/public');
|
mutate('/api/v1/settings/public');
|
||||||
@@ -63,21 +64,30 @@ const UserWebPushSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, isValid }) => {
|
{({
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
isSubmitting,
|
||||||
|
isValid,
|
||||||
|
values,
|
||||||
|
setFieldValue,
|
||||||
|
setFieldTouched,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Form className="section">
|
<Form className="section">
|
||||||
<div className="form-row">
|
<NotificationTypeSelector
|
||||||
<label htmlFor="enableEmail" className="checkbox-label">
|
user={user}
|
||||||
{intl.formatMessage(messages.enableWebPush)}
|
currentTypes={values.types}
|
||||||
</label>
|
onUpdate={(newTypes) => {
|
||||||
<div className="form-input">
|
setFieldValue('types', newTypes);
|
||||||
<Field
|
setFieldTouched('types');
|
||||||
type="checkbox"
|
}}
|
||||||
id="enableWebPush"
|
error={
|
||||||
name="enableWebPush"
|
errors.types && touched.types
|
||||||
/>
|
? (errors.types as string)
|
||||||
</div>
|
: undefined
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ const UserNotificationSettings: React.FC = ({ children }) => {
|
|||||||
regex: /\/settings\/notifications\/email/,
|
regex: /\/settings\/notifications\/email/,
|
||||||
hidden: !data?.emailEnabled,
|
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',
|
text: 'Discord',
|
||||||
content: (
|
content: (
|
||||||
@@ -65,18 +77,6 @@ const UserNotificationSettings: React.FC = ({ children }) => {
|
|||||||
regex: /\/settings\/notifications\/telegram/,
|
regex: /\/settings\/notifications\/telegram/,
|
||||||
hidden: !data?.telegramEnabled || !data?.telegramBotUsername,
|
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) => {
|
settingsRoutes.forEach((settingsRoute) => {
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const UserSettings: React.FC = ({ children }) => {
|
|||||||
text: intl.formatMessage(messages.menuNotifications),
|
text: intl.formatMessage(messages.menuNotifications),
|
||||||
route: data?.emailEnabled
|
route: data?.emailEnabled
|
||||||
? '/settings/notifications/email'
|
? '/settings/notifications/email'
|
||||||
|
: data?.webPushEnabled
|
||||||
|
? '/settings/notifications/webpush'
|
||||||
: '/settings/notifications/discord',
|
: '/settings/notifications/discord',
|
||||||
regex: /\/settings\/notifications/,
|
regex: /\/settings\/notifications/,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,18 +89,24 @@
|
|||||||
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
||||||
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
||||||
"components.NotificationTypeSelector.mediaAutoApproved": "Media Automatically Approved",
|
"components.NotificationTypeSelector.mediaAutoApproved": "Media Automatically Approved",
|
||||||
"components.NotificationTypeSelector.mediaAutoApprovedDescription": "Sends a notification when requested media is automatically approved.",
|
"components.NotificationTypeSelector.mediaAutoApprovedDescription": "Send notifications when users submit new media requests which are automatically approved.",
|
||||||
"components.NotificationTypeSelector.mediaapproved": "Media Approved",
|
"components.NotificationTypeSelector.mediaapproved": "Media Approved",
|
||||||
"components.NotificationTypeSelector.mediaapprovedDescription": "Sends a notification when requested media is manually approved.",
|
"components.NotificationTypeSelector.mediaapprovedDescription": "Send notifications when media requests are manually approved.",
|
||||||
"components.NotificationTypeSelector.mediaavailable": "Media Available",
|
"components.NotificationTypeSelector.mediaavailable": "Media Available",
|
||||||
"components.NotificationTypeSelector.mediaavailableDescription": "Sends a notification when requested media becomes available.",
|
"components.NotificationTypeSelector.mediaavailableDescription": "Send notifications when media requests become available.",
|
||||||
"components.NotificationTypeSelector.mediadeclined": "Media Declined",
|
"components.NotificationTypeSelector.mediadeclined": "Media Declined",
|
||||||
"components.NotificationTypeSelector.mediadeclinedDescription": "Sends a notification when a media request is declined.",
|
"components.NotificationTypeSelector.mediadeclinedDescription": "Send notifications when media requests are declined.",
|
||||||
"components.NotificationTypeSelector.mediafailed": "Media Failed",
|
"components.NotificationTypeSelector.mediafailed": "Media Failed",
|
||||||
"components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when requested media fails to be added to Radarr or Sonarr.",
|
"components.NotificationTypeSelector.mediafailedDescription": "Send notifications when media requests fail to be added to Radarr or Sonarr.",
|
||||||
"components.NotificationTypeSelector.mediarequested": "Media Requested",
|
"components.NotificationTypeSelector.mediarequested": "Media Requested",
|
||||||
"components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when media is requested and requires approval.",
|
"components.NotificationTypeSelector.mediarequestedDescription": "Send notifications when users submit new media requests which require approval.",
|
||||||
"components.NotificationTypeSelector.notificationTypes": "Notification Types",
|
"components.NotificationTypeSelector.notificationTypes": "Notification Types",
|
||||||
|
"components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Get notified when other users submit new media requests which are automatically approved.",
|
||||||
|
"components.NotificationTypeSelector.usermediaapprovedDescription": "Get notified when your media requests are approved.",
|
||||||
|
"components.NotificationTypeSelector.usermediaavailableDescription": "Get notified when your media requests become available.",
|
||||||
|
"components.NotificationTypeSelector.usermediadeclinedDescription": "Get notified when your media requests are declined.",
|
||||||
|
"components.NotificationTypeSelector.usermediafailedDescription": "Get notified when media requests fail to be added to Radarr or Sonarr.",
|
||||||
|
"components.NotificationTypeSelector.usermediarequestedDescription": "Get notified when other users submit new media requests which require approval.",
|
||||||
"components.PermissionEdit.admin": "Admin",
|
"components.PermissionEdit.admin": "Admin",
|
||||||
"components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.",
|
"components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.",
|
||||||
"components.PermissionEdit.advancedrequest": "Advanced Requests",
|
"components.PermissionEdit.advancedrequest": "Advanced Requests",
|
||||||
@@ -261,6 +267,7 @@
|
|||||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestFailed": "LunaSea test notification failed to send.",
|
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestFailed": "LunaSea test notification failed to send.",
|
||||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSending": "Sending LunaSea test notification…",
|
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSending": "Sending LunaSea test notification…",
|
||||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSuccess": "LunaSea test notification sent!",
|
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSuccess": "LunaSea test notification sent!",
|
||||||
|
"components.Settings.Notifications.NotificationsLunaSea.validationTypes": "You must select at least one notification type",
|
||||||
"components.Settings.Notifications.NotificationsLunaSea.validationWebhookUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.NotificationsLunaSea.validationWebhookUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.NotificationsLunaSea.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.NotificationsLunaSea.webhookUrl": "Webhook URL",
|
||||||
"components.Settings.Notifications.NotificationsLunaSea.webhookUrlTip": "Your user- or device-based <LunaSeaLink>notification webhook URL</LunaSeaLink>",
|
"components.Settings.Notifications.NotificationsLunaSea.webhookUrlTip": "Your user- or device-based <LunaSeaLink>notification webhook URL</LunaSeaLink>",
|
||||||
@@ -273,6 +280,7 @@
|
|||||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Sending Pushbullet test notification…",
|
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Sending Pushbullet test notification…",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet test notification sent!",
|
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet test notification sent!",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token",
|
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token",
|
||||||
|
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "You must select at least one notification type",
|
||||||
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token",
|
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token",
|
||||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr",
|
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr",
|
||||||
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
|
||||||
@@ -284,13 +292,15 @@
|
|||||||
"components.Settings.Notifications.NotificationsPushover.userToken": "User or Group Key",
|
"components.Settings.Notifications.NotificationsPushover.userToken": "User or Group Key",
|
||||||
"components.Settings.Notifications.NotificationsPushover.userTokenTip": "Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>",
|
"components.Settings.Notifications.NotificationsPushover.userTokenTip": "Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>",
|
||||||
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide a valid application token",
|
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide a valid application token",
|
||||||
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user key",
|
"components.Settings.Notifications.NotificationsPushover.validationTypes": "You must select at least one notification type",
|
||||||
|
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user or group key",
|
||||||
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Enable Agent",
|
||||||
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notification settings failed to save.",
|
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notification settings failed to save.",
|
||||||
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notification settings saved successfully!",
|
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "Slack test notification failed to send.",
|
"components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "Slack test notification failed to send.",
|
||||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "Sending Slack test notification…",
|
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "Sending Slack test notification…",
|
||||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Slack test notification sent!",
|
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Slack test notification sent!",
|
||||||
|
"components.Settings.Notifications.NotificationsSlack.validationTypes": "You must select at least one notification type",
|
||||||
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
|
||||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
|
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
|
||||||
@@ -311,6 +321,7 @@
|
|||||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Sending webhook test notification…",
|
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Sending webhook test notification…",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Webhook test notification sent!",
|
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Webhook test notification sent!",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "You must provide a valid JSON payload",
|
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "You must provide a valid JSON payload",
|
||||||
|
"components.Settings.Notifications.NotificationsWebhook.validationTypes": "You must select at least one notification type",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Webhook notification settings failed to save.",
|
"components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Webhook notification settings failed to save.",
|
||||||
@@ -328,8 +339,6 @@
|
|||||||
"components.Settings.Notifications.chatIdTip": "Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command",
|
"components.Settings.Notifications.chatIdTip": "Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command",
|
||||||
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
|
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
|
||||||
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!",
|
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.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.",
|
|
||||||
"components.Settings.Notifications.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.",
|
|
||||||
"components.Settings.Notifications.emailsender": "Sender Address",
|
"components.Settings.Notifications.emailsender": "Sender Address",
|
||||||
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
|
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
|
||||||
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!",
|
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!",
|
||||||
@@ -362,10 +371,11 @@
|
|||||||
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authorization token",
|
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authorization token",
|
||||||
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
|
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
|
||||||
"components.Settings.Notifications.validationEmail": "You must provide a valid email address",
|
"components.Settings.Notifications.validationEmail": "You must provide a valid email address",
|
||||||
"components.Settings.Notifications.validationPgpPassword": "You must provide a PGP password if a PGP private key is entered",
|
"components.Settings.Notifications.validationPgpPassword": "You must provide a PGP password",
|
||||||
"components.Settings.Notifications.validationPgpPrivateKey": "You must provide a valid PGP private key if a PGP password is entered",
|
"components.Settings.Notifications.validationPgpPrivateKey": "You must provide a valid PGP private key",
|
||||||
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide a valid hostname or IP address",
|
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide a valid hostname or IP address",
|
||||||
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide a valid port number",
|
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide a valid port number",
|
||||||
|
"components.Settings.Notifications.validationTypes": "You must select at least one notification type",
|
||||||
"components.Settings.Notifications.validationUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.validationUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.webhookUrl": "Webhook URL",
|
||||||
"components.Settings.Notifications.webhookUrlTip": "Create a <DiscordWebhookLink>webhook integration</DiscordWebhookLink> in your server",
|
"components.Settings.Notifications.webhookUrlTip": "Create a <DiscordWebhookLink>webhook integration</DiscordWebhookLink> in your server",
|
||||||
@@ -765,10 +775,6 @@
|
|||||||
"components.UserProfile.UserSettings.UserNotificationSettings.email": "Email",
|
"components.UserProfile.UserSettings.UserNotificationSettings.email": "Email",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "Email notification settings failed to save.",
|
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "Email notification settings failed to save.",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Email notification settings saved successfully!",
|
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Email notification settings saved successfully!",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.enableDiscord": "Enable Mentions",
|
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.enableEmail": "Enable Notifications",
|
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.enableTelegram": "Enable Notifications",
|
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.enableWebPush": "Enable Notifications",
|
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications",
|
"components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings",
|
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key",
|
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key",
|
||||||
|
|||||||
Reference in New Issue
Block a user