mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
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
|
||||
userToken:
|
||||
type: string
|
||||
priority:
|
||||
type: number
|
||||
LunaSeaSettings:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1599,6 +1597,9 @@ components:
|
||||
nullable: true
|
||||
discordEnabled:
|
||||
type: boolean
|
||||
discordEnabledTypes:
|
||||
type: number
|
||||
nullable: true
|
||||
discordId:
|
||||
type: string
|
||||
nullable: true
|
||||
|
@@ -108,7 +108,10 @@ export class UserSettings {
|
||||
})
|
||||
public notificationTypes: Partial<NotificationAgentTypes>;
|
||||
|
||||
public hasNotificationType(key: NotificationAgentKey, type: Notification) {
|
||||
public hasNotificationType(
|
||||
key: NotificationAgentKey,
|
||||
type: Notification
|
||||
): boolean {
|
||||
return hasNotificationType(type, this.notificationTypes[key] ?? 0);
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ export interface UserSettingsNotificationsResponse {
|
||||
emailEnabled?: boolean;
|
||||
pgpKey?: string;
|
||||
discordEnabled?: boolean;
|
||||
discordEnabledTypes?: number;
|
||||
discordId?: string;
|
||||
telegramEnabled?: boolean;
|
||||
telegramBotUsername?: string;
|
||||
|
@@ -24,6 +24,6 @@ export abstract class BaseAgent<T extends NotificationAgentConfig> {
|
||||
}
|
||||
|
||||
export interface NotificationAgent {
|
||||
shouldSend(type: Notification): boolean;
|
||||
shouldSend(): boolean;
|
||||
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
|
||||
}
|
||||
|
@@ -193,12 +193,10 @@ class DiscordAgent
|
||||
};
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.webhookUrl &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
) {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (settings.enabled && settings.options.webhookUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -209,6 +207,12 @@ class DiscordAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug('Sending Discord notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
@@ -218,13 +222,6 @@ class DiscordAgent
|
||||
let content = undefined;
|
||||
|
||||
try {
|
||||
const { botUsername, botAvatarUrl, webhookUrl } =
|
||||
this.getSettings().options;
|
||||
|
||||
if (!webhookUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.notifyUser) {
|
||||
// Mention user who submitted the request
|
||||
if (
|
||||
@@ -258,9 +255,9 @@ class DiscordAgent
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, {
|
||||
username: botUsername,
|
||||
avatar_url: botAvatarUrl,
|
||||
await axios.post(settings.options.webhookUrl, {
|
||||
username: settings.options.botUsername,
|
||||
avatar_url: settings.options.botAvatarUrl,
|
||||
embeds: [this.buildEmbed(type, payload)],
|
||||
content,
|
||||
} as DiscordWebhookPayload);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { EmailOptions } from 'email-templates';
|
||||
import path from 'path';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
@@ -28,12 +28,14 @@ class EmailAgent
|
||||
return settings.notifications.agents.email;
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
settings.enabled &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
settings.options.emailFrom &&
|
||||
settings.options.smtpHost &&
|
||||
settings.options.smtpPort
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@@ -50,12 +50,10 @@ class LunaSeaAgent
|
||||
};
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.webhookUrl &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
) {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (settings.enabled && settings.options.webhookUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -66,6 +64,12 @@ class LunaSeaAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug('Sending LunaSea notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
@@ -73,19 +77,19 @@ class LunaSeaAgent
|
||||
});
|
||||
|
||||
try {
|
||||
const { webhookUrl, profileName } = this.getSettings().options;
|
||||
|
||||
if (!webhookUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, this.buildPayload(type, payload), {
|
||||
await axios.post(
|
||||
settings.options.webhookUrl,
|
||||
this.buildPayload(type, payload),
|
||||
settings.options.profileName
|
||||
? {
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${profileName}:`).toString(
|
||||
'base64'
|
||||
)}`,
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${settings.options.profileName}:`
|
||||
).toString('base64')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -94,7 +98,7 @@ class LunaSeaAgent
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
response: e.response?.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
@@ -24,12 +24,10 @@ class PushbulletAgent
|
||||
return settings.notifications.agents.pushbullet;
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.accessToken &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
) {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (settings.enabled && settings.options.accessToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -137,6 +135,12 @@ class PushbulletAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug('Sending Pushbullet notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
@@ -144,14 +148,10 @@ class PushbulletAgent
|
||||
});
|
||||
|
||||
try {
|
||||
const endpoint = 'https://api.pushbullet.com/v2/pushes';
|
||||
|
||||
const { accessToken } = this.getSettings().options;
|
||||
|
||||
const { title, body } = this.constructMessageDetails(type, payload);
|
||||
|
||||
await axios.post(
|
||||
endpoint,
|
||||
'https://api.pushbullet.com/v2/pushes',
|
||||
{
|
||||
type: 'note',
|
||||
title: title,
|
||||
@@ -159,7 +159,7 @@ class PushbulletAgent
|
||||
} as PushbulletPayload,
|
||||
{
|
||||
headers: {
|
||||
'Access-Token': accessToken,
|
||||
'Access-Token': settings.options.accessToken,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@@ -30,12 +30,13 @@ class PushoverAgent
|
||||
return settings.notifications.agents.pushover;
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.accessToken &&
|
||||
this.getSettings().options.userToken &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
settings.enabled &&
|
||||
settings.options.accessToken &&
|
||||
settings.options.userToken
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -161,6 +162,12 @@ class PushoverAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug('Sending Pushover notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
@@ -169,14 +176,12 @@ class PushoverAgent
|
||||
try {
|
||||
const endpoint = 'https://api.pushover.net/1/messages.json';
|
||||
|
||||
const { accessToken, userToken } = this.getSettings().options;
|
||||
|
||||
const { title, message, url, url_title, priority } =
|
||||
this.constructMessageDetails(type, payload);
|
||||
|
||||
await axios.post(endpoint, {
|
||||
token: accessToken,
|
||||
user: userToken,
|
||||
token: settings.options.accessToken,
|
||||
user: settings.options.userToken,
|
||||
title: title,
|
||||
message: message,
|
||||
url: url,
|
||||
|
@@ -218,12 +218,10 @@ class SlackAgent
|
||||
};
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.webhookUrl &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
) {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (settings.enabled && settings.options.webhookUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -234,19 +232,22 @@ class SlackAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug('Sending Slack notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
try {
|
||||
const webhookUrl = this.getSettings().options.webhookUrl;
|
||||
|
||||
if (!webhookUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, this.buildEmbed(type, payload));
|
||||
await axios.post(
|
||||
settings.options.webhookUrl,
|
||||
this.buildEmbed(type, payload)
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import { Permission } from '../../permissions';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentKey,
|
||||
@@ -40,12 +43,13 @@ class TelegramAgent
|
||||
return settings.notifications.agents.telegram;
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.botAPI &&
|
||||
this.getSettings().options.chatId &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
settings.enabled &&
|
||||
settings.options.botAPI &&
|
||||
settings.options.chatId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -59,8 +63,10 @@ class TelegramAgent
|
||||
|
||||
private buildMessage(
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): string {
|
||||
payload: NotificationPayload,
|
||||
chatId: string,
|
||||
sendSilently: boolean
|
||||
): TelegramMessagePayload | TelegramPhotoPayload {
|
||||
const settings = getSettings();
|
||||
let message = '';
|
||||
|
||||
@@ -153,41 +159,49 @@ class TelegramAgent
|
||||
}
|
||||
/* 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(
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): 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'
|
||||
}`;
|
||||
|
||||
// Send system notification
|
||||
try {
|
||||
if (hasNotificationType(type, settings.types ?? 0)) {
|
||||
logger.debug('Sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
try {
|
||||
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)
|
||||
this.buildMessage(
|
||||
type,
|
||||
payload,
|
||||
settings.options.chatId,
|
||||
settings.options.sendSilently
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Error sending Telegram notification', {
|
||||
@@ -195,22 +209,23 @@ class TelegramAgent
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
response: e.response?.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.notifyUser) {
|
||||
// Send notification to the user who submitted the request
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
payload.notifyUser.settings?.hasNotificationType(
|
||||
NotificationAgentKey.TELEGRAM,
|
||||
type
|
||||
) &&
|
||||
payload.notifyUser.settings?.telegramChatId &&
|
||||
payload.notifyUser.settings?.telegramChatId !==
|
||||
this.getSettings().options.chatId
|
||||
payload.notifyUser.settings?.telegramChatId !== settings.options.chatId
|
||||
) {
|
||||
// Send notification to the user who submitted the request
|
||||
logger.debug('Sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
@@ -221,22 +236,12 @@ class TelegramAgent
|
||||
try {
|
||||
await axios.post(
|
||||
endpoint,
|
||||
payload.image
|
||||
? ({
|
||||
photo: payload.image,
|
||||
caption: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
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)
|
||||
this.buildMessage(
|
||||
type,
|
||||
payload,
|
||||
payload.notifyUser.settings.telegramChatId,
|
||||
!!payload.notifyUser.settings.telegramSendSilently
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Error sending Telegram notification', {
|
||||
@@ -251,6 +256,62 @@ class TelegramAgent
|
||||
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;
|
||||
}
|
||||
|
@@ -113,12 +113,10 @@ class WebhookAgent
|
||||
return this.parseKeys(parsedJSON, payload, type);
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
this.getSettings().options.webhookUrl &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
) {
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (settings.enabled && settings.options.webhookUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -129,6 +127,12 @@ class WebhookAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug('Sending webhook notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
@@ -136,17 +140,17 @@ class WebhookAgent
|
||||
});
|
||||
|
||||
try {
|
||||
const { webhookUrl, authHeader } = this.getSettings().options;
|
||||
|
||||
if (!webhookUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, this.buildPayload(type, payload), {
|
||||
await axios.post(
|
||||
settings.options.webhookUrl,
|
||||
this.buildPayload(type, payload),
|
||||
settings.options.authHeader
|
||||
? {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
Authorization: settings.options.authHeader,
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { getRepository } from 'typeorm';
|
||||
import webpush from 'web-push';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import { User } from '../../../entity/User';
|
||||
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
|
||||
@@ -135,11 +135,8 @@ class WebPushAgent
|
||||
}
|
||||
}
|
||||
|
||||
public shouldSend(type: Notification): boolean {
|
||||
if (
|
||||
this.getSettings().enabled &&
|
||||
hasNotificationType(type, this.getSettings().types)
|
||||
) {
|
||||
public shouldSend(): boolean {
|
||||
if (this.getSettings().enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -150,11 +147,6 @@ class WebPushAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending web push notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
const userRepository = getRepository(User);
|
||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||
const settings = getSettings();
|
||||
@@ -213,8 +205,15 @@ class WebPushAgent
|
||||
settings.vapidPrivate
|
||||
);
|
||||
|
||||
Promise.all(
|
||||
await Promise.all(
|
||||
pushSubs.map(async (sub) => {
|
||||
logger.debug('Sending web push notification', {
|
||||
label: 'Notifications',
|
||||
recipient: sub.user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
{
|
||||
@@ -230,12 +229,24 @@ class WebPushAgent
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Error sending web push notification; removing subscription',
|
||||
{
|
||||
label: 'Notifications',
|
||||
recipient: sub.user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
// Failed to send notification so we need to remove the subscription
|
||||
userPushSubRepository.remove(sub);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,11 @@ export const hasNotificationType = (
|
||||
total = types;
|
||||
}
|
||||
|
||||
// Test notifications don't need to be enabled
|
||||
if (!(value & Notification.TEST_NOTIFICATION)) {
|
||||
value += Notification.TEST_NOTIFICATION;
|
||||
}
|
||||
|
||||
return !!(value & total);
|
||||
};
|
||||
|
||||
@@ -51,7 +56,7 @@ class NotificationManager {
|
||||
});
|
||||
|
||||
this.activeAgents.forEach((agent) => {
|
||||
if (agent.shouldSend(type)) {
|
||||
if (agent.shouldSend()) {
|
||||
agent.send(type, payload);
|
||||
}
|
||||
});
|
||||
|
@@ -113,7 +113,7 @@ interface FullPublicSettings extends PublicSettings {
|
||||
|
||||
export interface NotificationAgentConfig {
|
||||
enabled: boolean;
|
||||
types: number;
|
||||
types?: number;
|
||||
options: Record<string, unknown>;
|
||||
}
|
||||
export interface NotificationAgentDiscord extends NotificationAgentConfig {
|
||||
@@ -150,7 +150,7 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
|
||||
export interface NotificationAgentLunaSea extends NotificationAgentConfig {
|
||||
options: {
|
||||
webhookUrl: string;
|
||||
profileName: string;
|
||||
profileName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,7 +173,6 @@ export interface NotificationAgentPushover extends NotificationAgentConfig {
|
||||
options: {
|
||||
accessToken: string;
|
||||
userToken: string;
|
||||
priority: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,7 +180,7 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
|
||||
options: {
|
||||
webhookUrl: string;
|
||||
jsonPayload: string;
|
||||
authHeader: string;
|
||||
authHeader?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -272,7 +271,6 @@ class Settings {
|
||||
agents: {
|
||||
email: {
|
||||
enabled: false,
|
||||
types: 0,
|
||||
options: {
|
||||
emailFrom: '',
|
||||
smtpHost: '',
|
||||
@@ -288,8 +286,6 @@ class Settings {
|
||||
enabled: false,
|
||||
types: 0,
|
||||
options: {
|
||||
botUsername: '',
|
||||
botAvatarUrl: '',
|
||||
webhookUrl: '',
|
||||
},
|
||||
},
|
||||
@@ -298,7 +294,6 @@ class Settings {
|
||||
types: 0,
|
||||
options: {
|
||||
webhookUrl: '',
|
||||
profileName: '',
|
||||
},
|
||||
},
|
||||
slack: {
|
||||
@@ -312,7 +307,6 @@ class Settings {
|
||||
enabled: false,
|
||||
types: 0,
|
||||
options: {
|
||||
botUsername: '',
|
||||
botAPI: '',
|
||||
chatId: '',
|
||||
sendSilently: false,
|
||||
@@ -331,7 +325,6 @@ class Settings {
|
||||
options: {
|
||||
accessToken: '',
|
||||
userToken: '',
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
webhook: {
|
||||
@@ -339,14 +332,12 @@ class Settings {
|
||||
types: 0,
|
||||
options: {
|
||||
webhookUrl: '',
|
||||
authHeader: '',
|
||||
jsonPayload:
|
||||
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
|
||||
},
|
||||
},
|
||||
webpush: {
|
||||
enabled: false,
|
||||
types: 0,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
|
@@ -238,7 +238,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
isOwnProfileOrAdmin(),
|
||||
async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const settings = getSettings();
|
||||
const settings = getSettings()?.notifications.agents;
|
||||
|
||||
try {
|
||||
const user = await userRepository.findOne({
|
||||
@@ -250,16 +250,18 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
emailEnabled: settings?.notifications.agents.email.enabled,
|
||||
emailEnabled: settings?.email.enabled,
|
||||
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,
|
||||
telegramEnabled: settings?.notifications.agents.telegram.enabled,
|
||||
telegramBotUsername:
|
||||
settings?.notifications.agents.telegram.options.botUsername,
|
||||
telegramEnabled: settings?.telegram.enabled,
|
||||
telegramBotUsername: settings?.telegram.options.botUsername,
|
||||
telegramChatId: user.settings?.telegramChatId,
|
||||
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||
webPushEnabled: settings?.notifications.agents.webpush.enabled,
|
||||
webPushEnabled: settings?.webpush.enabled,
|
||||
notificationTypes: user.settings?.notificationTypes ?? {},
|
||||
});
|
||||
} 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
|
||||
);
|
||||
}}
|
||||
defaultChecked={
|
||||
checked={
|
||||
hasNotificationType(option.value, currentTypes) ||
|
||||
(!!parent?.value &&
|
||||
hasNotificationType(parent.value, currentTypes))
|
||||
@@ -46,10 +46,12 @@ const NotificationType: React.FC<NotificationTypeProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm leading-6">
|
||||
<label htmlFor={option.id} className="font-medium text-white">
|
||||
{option.name}
|
||||
<label htmlFor={option.id} className="block font-medium text-white">
|
||||
<div className="flex flex-col">
|
||||
<span>{option.name}</span>
|
||||
<span className="text-gray-500">{option.description}</span>
|
||||
</div>
|
||||
</label>
|
||||
<p className="text-gray-500">{option.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{(option.children ?? []).map((child) => (
|
||||
|
@@ -1,27 +1,42 @@
|
||||
import React from 'react';
|
||||
import { sortBy } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import { Permission, User, useUser } from '../../hooks/useUser';
|
||||
import NotificationType from './NotificationType';
|
||||
|
||||
const messages = defineMessages({
|
||||
notificationTypes: 'Notification Types',
|
||||
mediarequested: 'Media Requested',
|
||||
mediarequestedDescription:
|
||||
'Sends a notification when media is requested and requires approval.',
|
||||
'Send notifications when users submit new media requests which require approval.',
|
||||
usermediarequestedDescription:
|
||||
'Get notified when other users submit new media requests which require approval.',
|
||||
mediaapproved: 'Media Approved',
|
||||
mediaapprovedDescription:
|
||||
'Sends a notification when requested media is manually approved.',
|
||||
'Send notifications when media requests are manually approved.',
|
||||
usermediaapprovedDescription:
|
||||
'Get notified when your media requests are approved.',
|
||||
mediaAutoApproved: 'Media Automatically Approved',
|
||||
mediaAutoApprovedDescription:
|
||||
'Sends a notification when requested media is automatically approved.',
|
||||
'Send notifications when users submit new media requests which are automatically approved.',
|
||||
usermediaAutoApprovedDescription:
|
||||
'Get notified when other users submit new media requests which are automatically approved.',
|
||||
mediaavailable: 'Media Available',
|
||||
mediaavailableDescription:
|
||||
'Sends a notification when requested media becomes available.',
|
||||
'Send notifications when media requests become available.',
|
||||
usermediaavailableDescription:
|
||||
'Get notified when your media requests become available.',
|
||||
mediafailed: 'Media Failed',
|
||||
mediafailedDescription:
|
||||
'Sends a notification when requested media fails to be added to Radarr or Sonarr.',
|
||||
'Send notifications when media requests fail to be added to Radarr or Sonarr.',
|
||||
usermediafailedDescription:
|
||||
'Get notified when media requests fail to be added to Radarr or Sonarr.',
|
||||
mediadeclined: 'Media Declined',
|
||||
mediadeclinedDescription:
|
||||
'Sends a notification when a media request is declined.',
|
||||
'Send notifications when media requests are declined.',
|
||||
usermediadeclinedDescription:
|
||||
'Get notified when your media requests are declined.',
|
||||
});
|
||||
|
||||
export const hasNotificationType = (
|
||||
@@ -30,16 +45,23 @@ export const hasNotificationType = (
|
||||
): boolean => {
|
||||
let total = 0;
|
||||
|
||||
// If we are not checking any notifications, bail out and return true
|
||||
if (types === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(types)) {
|
||||
// Combine all notification values into one
|
||||
total = types.reduce((a, v) => a + v, 0);
|
||||
} else {
|
||||
total = types;
|
||||
}
|
||||
|
||||
// Test notifications don't need to be enabled
|
||||
if (!(value & Notification.TEST_NOTIFICATION)) {
|
||||
value += Notification.TEST_NOTIFICATION;
|
||||
}
|
||||
|
||||
return !!(value & total);
|
||||
};
|
||||
|
||||
@@ -63,69 +85,183 @@ export interface NotificationItem {
|
||||
name: string;
|
||||
description: string;
|
||||
value: Notification;
|
||||
hasNotifyUser?: boolean;
|
||||
children?: NotificationItem[];
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
interface NotificationTypeSelectorProps {
|
||||
user?: User;
|
||||
enabledTypes?: number;
|
||||
currentTypes: number;
|
||||
onUpdate: (newTypes: number) => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
||||
user,
|
||||
enabledTypes = ALL_NOTIFICATIONS,
|
||||
currentTypes,
|
||||
onUpdate,
|
||||
error,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { hasPermission } = useUser({ id: user?.id });
|
||||
const [allowedTypes, setAllowedTypes] = useState(enabledTypes);
|
||||
|
||||
const availableTypes = useMemo(() => {
|
||||
const allRequestsAutoApproved =
|
||||
user &&
|
||||
// Has Manage Requests perm, which grants all Auto-Approve perms
|
||||
(hasPermission(Permission.MANAGE_REQUESTS) ||
|
||||
// Cannot submit requests of any type
|
||||
!hasPermission(
|
||||
[
|
||||
Permission.REQUEST,
|
||||
Permission.REQUEST_MOVIE,
|
||||
Permission.REQUEST_TV,
|
||||
Permission.REQUEST_4K,
|
||||
Permission.REQUEST_4K_MOVIE,
|
||||
Permission.REQUEST_4K_TV,
|
||||
],
|
||||
{ type: 'or' }
|
||||
) ||
|
||||
// Cannot submit non-4K movie requests OR has Auto-Approve perms for non-4K movies
|
||||
((!hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], {
|
||||
type: 'or',
|
||||
}) ||
|
||||
hasPermission(
|
||||
[Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_MOVIE],
|
||||
{ type: 'or' }
|
||||
)) &&
|
||||
// Cannot submit non-4K series requests OR has Auto-Approve perms for non-4K series
|
||||
(!hasPermission([Permission.REQUEST, Permission.REQUEST_TV], {
|
||||
type: 'or',
|
||||
}) ||
|
||||
hasPermission(
|
||||
[Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_TV],
|
||||
{ type: 'or' }
|
||||
)) &&
|
||||
// Cannot submit 4K movie requests OR has Auto-Approve perms for 4K movies
|
||||
(!settings.currentSettings.movie4kEnabled ||
|
||||
!hasPermission(
|
||||
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
|
||||
{ type: 'or' }
|
||||
) ||
|
||||
hasPermission(
|
||||
[Permission.AUTO_APPROVE_4K, Permission.AUTO_APPROVE_4K_MOVIE],
|
||||
{ type: 'or' }
|
||||
)) &&
|
||||
// Cannot submit 4K series requests OR has Auto-Approve perms for 4K series
|
||||
(!settings.currentSettings.series4kEnabled ||
|
||||
!hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
|
||||
type: 'or',
|
||||
}) ||
|
||||
hasPermission(
|
||||
[Permission.AUTO_APPROVE_4K, Permission.AUTO_APPROVE_4K_TV],
|
||||
{ type: 'or' }
|
||||
))));
|
||||
|
||||
const types: NotificationItem[] = [
|
||||
{
|
||||
id: 'media-requested',
|
||||
name: intl.formatMessage(messages.mediarequested),
|
||||
description: intl.formatMessage(messages.mediarequestedDescription),
|
||||
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(messages.mediaAutoApprovedDescription),
|
||||
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(messages.mediaapprovedDescription),
|
||||
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(messages.mediadeclinedDescription),
|
||||
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(messages.mediaavailableDescription),
|
||||
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(messages.mediafailedDescription),
|
||||
description: intl.formatMessage(
|
||||
user
|
||||
? messages.usermediafailedDescription
|
||||
: messages.mediafailedDescription
|
||||
),
|
||||
value: Notification.MEDIA_FAILED,
|
||||
hidden: user && !hasPermission(Permission.MANAGE_REQUESTS),
|
||||
},
|
||||
];
|
||||
|
||||
const filteredTypes = types.filter(
|
||||
(type) => !type.hidden && hasNotificationType(type.value, enabledTypes)
|
||||
);
|
||||
|
||||
const newAllowedTypes = filteredTypes.reduce((a, v) => a + v.value, 0);
|
||||
if (newAllowedTypes !== allowedTypes) {
|
||||
setAllowedTypes(newAllowedTypes);
|
||||
}
|
||||
|
||||
return user
|
||||
? sortBy(filteredTypes, 'hasNotifyUser', 'DESC')
|
||||
: filteredTypes;
|
||||
}, [user, hasPermission, settings, intl, allowedTypes, enabledTypes]);
|
||||
|
||||
if (!availableTypes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div role="group" aria-labelledby="group-label" className="form-group">
|
||||
<div className="form-row">
|
||||
<span id="group-label" className="group-label">
|
||||
{intl.formatMessage(messages.notificationTypes)}
|
||||
<span className="label-required">*</span>
|
||||
{!user && <span className="label-required">*</span>}
|
||||
</span>
|
||||
<div className="form-input">
|
||||
<div className="max-w-lg">
|
||||
{types.map((type) => (
|
||||
{availableTypes.map((type) => (
|
||||
<NotificationType
|
||||
key={`notification-type-${type.id}`}
|
||||
option={type}
|
||||
@@ -134,6 +270,7 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
||||
toastDiscordTestSuccess: 'Discord test notification sent!',
|
||||
toastDiscordTestFailed: 'Discord test notification failed to send.',
|
||||
validationUrl: 'You must provide a valid URL',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
|
||||
const NotificationsDiscord: React.FC = () => {
|
||||
@@ -46,6 +47,13 @@ const NotificationsDiscord: React.FC = () => {
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.url(intl.formatMessage(messages.validationUrl)),
|
||||
types: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.moreThan(0, intl.formatMessage(messages.validationTypes)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -88,7 +96,15 @@ const NotificationsDiscord: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
values,
|
||||
isValid,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -211,8 +227,20 @@ const NotificationsDiscord: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -6,12 +6,10 @@ import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import globalMessages from '../../../i18n/globalMessages';
|
||||
import Alert from '../../Common/Alert';
|
||||
import Badge from '../../Common/Badge';
|
||||
import Button from '../../Common/Button';
|
||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||
import SensitiveInput from '../../Common/SensitiveInput';
|
||||
import NotificationTypeSelector from '../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
||||
@@ -37,20 +35,14 @@ const messages = defineMessages({
|
||||
allowselfsigned: 'Allow Self-Signed Certificates',
|
||||
senderName: 'Sender Name',
|
||||
validationEmail: 'You must provide a valid email address',
|
||||
emailNotificationTypesAlertDescription:
|
||||
'<strong>Media Requested</strong>, <strong>Media Automatically Approved</strong>, and <strong>Media Failed</strong> email notifications are sent to all users with the <strong>Manage Requests</strong> permission.',
|
||||
emailNotificationTypesAlertDescriptionPt2:
|
||||
'<strong>Media Approved</strong>, <strong>Media Declined</strong>, and <strong>Media Available</strong> email notifications are sent to the user who submitted the request.',
|
||||
pgpPrivateKey: 'PGP Private Key',
|
||||
pgpPrivateKeyTip:
|
||||
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||
validationPgpPrivateKey:
|
||||
'You must provide a valid PGP private key if a PGP password is entered',
|
||||
validationPgpPrivateKey: 'You must provide a valid PGP private key',
|
||||
pgpPassword: 'PGP Password',
|
||||
pgpPasswordTip:
|
||||
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||
validationPgpPassword:
|
||||
'You must provide a PGP password if a PGP private key is entered',
|
||||
validationPgpPassword: 'You must provide a PGP password',
|
||||
});
|
||||
|
||||
export function OpenPgpLink(msg: string): JSX.Element {
|
||||
@@ -130,7 +122,6 @@ const NotificationsEmail: React.FC = () => {
|
||||
<Formik
|
||||
initialValues={{
|
||||
enabled: data.enabled,
|
||||
types: data.types,
|
||||
emailFrom: data.options.emailFrom,
|
||||
smtpHost: data.options.smtpHost,
|
||||
smtpPort: data.options.smtpPort ?? 587,
|
||||
@@ -153,7 +144,6 @@ const NotificationsEmail: React.FC = () => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/email', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {
|
||||
emailFrom: values.emailFrom,
|
||||
smtpHost: values.smtpHost,
|
||||
@@ -184,7 +174,7 @@ const NotificationsEmail: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({ errors, touched, isSubmitting, values, isValid }) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -201,7 +191,6 @@ const NotificationsEmail: React.FC = () => {
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/email/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {
|
||||
emailFrom: values.emailFrom,
|
||||
smtpHost: values.smtpHost,
|
||||
@@ -238,42 +227,6 @@ const NotificationsEmail: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
title={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{intl.formatMessage(
|
||||
messages.emailNotificationTypesAlertDescription,
|
||||
{
|
||||
strong: function strong(msg) {
|
||||
return (
|
||||
<strong className="font-semibold text-indigo-100">
|
||||
{msg}
|
||||
</strong>
|
||||
);
|
||||
},
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages.emailNotificationTypesAlertDescriptionPt2,
|
||||
{
|
||||
strong: function strong(msg) {
|
||||
return (
|
||||
<strong className="font-semibold text-indigo-100">
|
||||
{msg}
|
||||
</strong>
|
||||
);
|
||||
},
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
type="info"
|
||||
/>
|
||||
<Form className="section">
|
||||
<div className="form-row">
|
||||
<label htmlFor="enabled" className="checkbox-label">
|
||||
@@ -354,6 +307,9 @@ const NotificationsEmail: React.FC = () => {
|
||||
<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">
|
||||
@@ -365,9 +321,7 @@ const NotificationsEmail: React.FC = () => {
|
||||
{intl.formatMessage(messages.encryptionDefault)}
|
||||
</option>
|
||||
<option value="opportunistic">
|
||||
{intl.formatMessage(
|
||||
messages.encryptionOpportunisticTls
|
||||
)}
|
||||
{intl.formatMessage(messages.encryptionOpportunisticTls)}
|
||||
</option>
|
||||
<option value="implicit">
|
||||
{intl.formatMessage(messages.encryptionImplicitTls)}
|
||||
@@ -471,10 +425,6 @@ const NotificationsEmail: React.FC = () => {
|
||||
)}
|
||||
</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">
|
||||
@@ -505,7 +455,6 @@ const NotificationsEmail: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
|
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
||||
toastLunaSeaTestSending: 'Sending LunaSea test notification…',
|
||||
toastLunaSeaTestSuccess: 'LunaSea test notification sent!',
|
||||
toastLunaSeaTestFailed: 'LunaSea test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
|
||||
const NotificationsLunaSea: React.FC = () => {
|
||||
@@ -43,6 +44,13 @@ const NotificationsLunaSea: React.FC = () => {
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.url(intl.formatMessage(messages.validationWebhookUrl)),
|
||||
types: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.moreThan(0, intl.formatMessage(messages.validationTypes)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -82,7 +90,15 @@ const NotificationsLunaSea: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
values,
|
||||
isValid,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -190,8 +206,20 @@ const NotificationsLunaSea: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
||||
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
|
||||
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
|
||||
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
|
||||
const NotificationsPushbullet: React.FC = () => {
|
||||
@@ -41,6 +42,13 @@ const NotificationsPushbullet: React.FC = () => {
|
||||
.required(intl.formatMessage(messages.validationAccessTokenRequired)),
|
||||
otherwise: Yup.string().nullable(),
|
||||
}),
|
||||
types: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.moreThan(0, intl.formatMessage(messages.validationTypes)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -78,7 +86,15 @@ const NotificationsPushbullet: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
values,
|
||||
isValid,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -170,8 +186,20 @@ const NotificationsPushbullet: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -19,12 +19,13 @@ const messages = defineMessages({
|
||||
userTokenTip:
|
||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||
validationAccessTokenRequired: 'You must provide a valid application token',
|
||||
validationUserTokenRequired: 'You must provide a valid user key',
|
||||
validationUserTokenRequired: 'You must provide a valid user or group key',
|
||||
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
||||
toastPushoverTestSending: 'Sending Pushover test notification…',
|
||||
toastPushoverTestSuccess: 'Pushover test notification sent!',
|
||||
toastPushoverTestFailed: 'Pushover test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
|
||||
const NotificationsPushover: React.FC = () => {
|
||||
@@ -60,6 +61,13 @@ const NotificationsPushover: React.FC = () => {
|
||||
/^[a-z\d]{30}$/i,
|
||||
intl.formatMessage(messages.validationUserTokenRequired)
|
||||
),
|
||||
types: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.moreThan(0, intl.formatMessage(messages.validationTypes)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -99,7 +107,15 @@ const NotificationsPushover: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
values,
|
||||
isValid,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -216,8 +232,20 @@ const NotificationsPushover: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -21,6 +21,7 @@ const messages = defineMessages({
|
||||
toastSlackTestSuccess: 'Slack test notification sent!',
|
||||
toastSlackTestFailed: 'Slack test notification failed to send.',
|
||||
validationWebhookUrl: 'You must provide a valid URL',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
|
||||
const NotificationsSlack: React.FC = () => {
|
||||
@@ -41,6 +42,13 @@ const NotificationsSlack: React.FC = () => {
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.url(intl.formatMessage(messages.validationWebhookUrl)),
|
||||
types: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.moreThan(0, intl.formatMessage(messages.validationTypes)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -78,7 +86,15 @@ const NotificationsSlack: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
values,
|
||||
isValid,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -168,8 +184,20 @@ const NotificationsSlack: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -105,7 +105,15 @@ const NotificationsTelegram: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
values,
|
||||
isValid,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -232,6 +240,24 @@ const NotificationsTelegram: React.FC = () => {
|
||||
<label htmlFor="chatId" className="text-label">
|
||||
{intl.formatMessage(messages.chatId)}
|
||||
<span className="label-required">*</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.chatIdTip, {
|
||||
GetIdBotLink: function GetIdBotLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://telegram.me/get_id_bot"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code: function code(msg) {
|
||||
return <code>{msg}</code>;
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
@@ -254,8 +280,20 @@ const NotificationsTelegram: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -8,7 +8,6 @@ import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Alert from '../../../Common/Alert';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
@@ -49,13 +48,11 @@ const NotificationsWebPush: React.FC = () => {
|
||||
<Formik
|
||||
initialValues={{
|
||||
enabled: data.enabled,
|
||||
types: data.types,
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/webpush', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {},
|
||||
});
|
||||
mutate('/api/v1/settings/public');
|
||||
@@ -73,7 +70,7 @@ const NotificationsWebPush: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, values, isValid, setFieldValue }) => {
|
||||
{({ isSubmitting }) => {
|
||||
const testSettings = async () => {
|
||||
setIsTesting(true);
|
||||
let toastId: string | undefined;
|
||||
@@ -90,7 +87,6 @@ const NotificationsWebPush: React.FC = () => {
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/webpush/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {},
|
||||
});
|
||||
|
||||
@@ -125,16 +121,12 @@ const NotificationsWebPush: React.FC = () => {
|
||||
<Field type="checkbox" id="enabled" name="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="warning"
|
||||
disabled={isSubmitting || !isValid || isTesting}
|
||||
disabled={isSubmitting || isTesting}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
testSettings();
|
||||
@@ -149,7 +141,7 @@ const NotificationsWebPush: React.FC = () => {
|
||||
<Button
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isValid || isTesting}
|
||||
disabled={isSubmitting || isTesting}
|
||||
>
|
||||
{isSubmitting
|
||||
? intl.formatMessage(globalMessages.saving)
|
||||
|
@@ -55,6 +55,7 @@ const messages = defineMessages({
|
||||
customJson: 'JSON Payload',
|
||||
templatevariablehelp: 'Template Variable Help',
|
||||
validationWebhookUrl: 'You must provide a valid URL',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
|
||||
const NotificationsWebhook: React.FC = () => {
|
||||
@@ -99,6 +100,13 @@ const NotificationsWebhook: React.FC = () => {
|
||||
}
|
||||
}
|
||||
),
|
||||
types: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.moreThan(0, intl.formatMessage(messages.validationTypes)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -293,8 +301,20 @@ const NotificationsWebhook: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
if (newTypes) {
|
||||
setFieldValue('enabled', true);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
@@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
discordsettingssaved: 'Discord notification settings saved successfully!',
|
||||
discordsettingsfailed: 'Discord notification settings failed to save.',
|
||||
enableDiscord: 'Enable Mentions',
|
||||
discordId: 'User ID',
|
||||
discordIdTip:
|
||||
'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your user account',
|
||||
@@ -34,8 +33,8 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
|
||||
const UserNotificationsDiscordSchema = Yup.object().shape({
|
||||
discordId: Yup.string()
|
||||
.when('enableDiscord', {
|
||||
is: true,
|
||||
.when('types', {
|
||||
is: (value: unknown) => !!value,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationDiscordId)),
|
||||
@@ -51,8 +50,10 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
enableDiscord: !!data?.notificationTypes.discord,
|
||||
discordId: data?.discordId,
|
||||
types:
|
||||
(data?.discordEnabledTypes ?? 0) &
|
||||
(data?.notificationTypes.discord ?? 0),
|
||||
}}
|
||||
validationSchema={UserNotificationsDiscordSchema}
|
||||
enableReinitialize
|
||||
@@ -64,7 +65,7 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
discord: values.enableDiscord ? ALL_NOTIFICATIONS : 0,
|
||||
discord: values.types,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
||||
@@ -81,27 +82,23 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, isValid }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section">
|
||||
{data?.discordEnabled && (
|
||||
<div className="form-row">
|
||||
<label htmlFor="enableDiscord" className="checkbox-label">
|
||||
{intl.formatMessage(messages.enableDiscord)}
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enableDiscord"
|
||||
name="enableDiscord"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-row">
|
||||
<label htmlFor="discordId" className="text-label">
|
||||
<span>{intl.formatMessage(messages.discordId)}</span>
|
||||
{intl.formatMessage(messages.discordId)}
|
||||
{!!data?.discordEnabledTypes && (
|
||||
<span className="label-required">*</span>
|
||||
)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.discordIdTip, {
|
||||
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
||||
@@ -127,6 +124,20 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
user={user}
|
||||
enabledTypes={data?.discordEnabledTypes ?? 0}
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
@@ -12,13 +12,15 @@ import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Badge from '../../../Common/Badge';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
||||
import SensitiveInput from '../../../Common/SensitiveInput';
|
||||
import NotificationTypeSelector, {
|
||||
ALL_NOTIFICATIONS,
|
||||
} from '../../../NotificationTypeSelector';
|
||||
import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail';
|
||||
|
||||
const messages = defineMessages({
|
||||
emailsettingssaved: 'Email notification settings saved successfully!',
|
||||
emailsettingsfailed: 'Email notification settings failed to save.',
|
||||
enableEmail: 'Enable Notifications',
|
||||
pgpPublicKey: 'PGP Public Key',
|
||||
pgpPublicKeyTip:
|
||||
'Encrypt email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||
@@ -50,8 +52,8 @@ const UserEmailSettings: React.FC = () => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
enableEmail: !!(data?.notificationTypes.email ?? true),
|
||||
pgpKey: data?.pgpKey,
|
||||
types: data?.notificationTypes.email ?? ALL_NOTIFICATIONS,
|
||||
}}
|
||||
validationSchema={UserNotificationsEmailSchema}
|
||||
enableReinitialize
|
||||
@@ -63,7 +65,7 @@ const UserEmailSettings: React.FC = () => {
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
email: values.enableEmail ? ALL_NOTIFICATIONS : 0,
|
||||
email: values.types,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
||||
@@ -80,17 +82,17 @@ const UserEmailSettings: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, isValid }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section">
|
||||
<div className="form-row">
|
||||
<label htmlFor="enableEmail" className="checkbox-label">
|
||||
{intl.formatMessage(messages.enableEmail)}
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<Field type="checkbox" id="enableEmail" name="enableEmail" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="pgpKey" className="text-label">
|
||||
<span className="mr-2">
|
||||
@@ -107,8 +109,9 @@ const UserEmailSettings: React.FC = () => {
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="textarea"
|
||||
<SensitiveInput
|
||||
as="field"
|
||||
type="textarea"
|
||||
id="pgpKey"
|
||||
name="pgpKey"
|
||||
rows="10"
|
||||
@@ -120,6 +123,19 @@ const UserEmailSettings: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
user={user}
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
|
@@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
||||
telegramsettingsfailed: 'Telegram notification settings failed to save.',
|
||||
enableTelegram: 'Enable Notifications',
|
||||
telegramChatId: 'Chat ID',
|
||||
telegramChatIdTipLong:
|
||||
'<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
||||
@@ -36,8 +35,8 @@ const UserTelegramSettings: React.FC = () => {
|
||||
|
||||
const UserNotificationsTelegramSchema = Yup.object().shape({
|
||||
telegramChatId: Yup.string()
|
||||
.when('enableTelegram', {
|
||||
is: true,
|
||||
.when('types', {
|
||||
is: (value: unknown) => !!value,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationTelegramChatId)),
|
||||
@@ -56,9 +55,9 @@ const UserTelegramSettings: React.FC = () => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
enableTelegram: !!data?.notificationTypes.telegram,
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
types: data?.notificationTypes.telegram ?? 0,
|
||||
}}
|
||||
validationSchema={UserNotificationsTelegramSchema}
|
||||
enableReinitialize
|
||||
@@ -70,7 +69,7 @@ const UserTelegramSettings: React.FC = () => {
|
||||
telegramChatId: values.telegramChatId,
|
||||
telegramSendSilently: values.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
telegram: values.enableTelegram ? ALL_NOTIFICATIONS : 0,
|
||||
telegram: values.types,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
||||
@@ -87,21 +86,17 @@ const UserTelegramSettings: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, isValid }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section">
|
||||
<div className="form-row">
|
||||
<label htmlFor="enableTelegram" className="checkbox-label">
|
||||
{intl.formatMessage(messages.enableTelegram)}
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enableTelegram"
|
||||
name="enableTelegram"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="telegramChatId" className="text-label">
|
||||
{intl.formatMessage(messages.telegramChatId)}
|
||||
@@ -166,6 +161,19 @@ const UserTelegramSettings: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
user={user}
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
@@ -10,12 +10,13 @@ import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
|
||||
import NotificationTypeSelector, {
|
||||
ALL_NOTIFICATIONS,
|
||||
} from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
||||
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
||||
enableWebPush: 'Enable Notifications',
|
||||
});
|
||||
|
||||
const UserWebPushSettings: React.FC = () => {
|
||||
@@ -34,18 +35,18 @@ const UserWebPushSettings: React.FC = () => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
enableWebPush: !!(data?.notificationTypes.webpush ?? true),
|
||||
pgpKey: data?.pgpKey,
|
||||
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
|
||||
}}
|
||||
enableReinitialize
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||
pgpKey: data?.pgpKey,
|
||||
discordId: data?.discordId,
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
webpush: values.enableWebPush ? ALL_NOTIFICATIONS : 0,
|
||||
webpush: values.types,
|
||||
},
|
||||
});
|
||||
mutate('/api/v1/settings/public');
|
||||
@@ -63,21 +64,30 @@ const UserWebPushSettings: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, isValid }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section">
|
||||
<div className="form-row">
|
||||
<label htmlFor="enableEmail" className="checkbox-label">
|
||||
{intl.formatMessage(messages.enableWebPush)}
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enableWebPush"
|
||||
name="enableWebPush"
|
||||
<NotificationTypeSelector
|
||||
user={user}
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
|
@@ -42,6 +42,18 @@ const UserNotificationSettings: React.FC = ({ children }) => {
|
||||
regex: /\/settings\/notifications\/email/,
|
||||
hidden: !data?.emailEnabled,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.webpush),
|
||||
content: (
|
||||
<span className="flex items-center">
|
||||
<CloudIcon className="h-4 mr-2" />
|
||||
{intl.formatMessage(messages.webpush)}
|
||||
</span>
|
||||
),
|
||||
route: '/settings/notifications/webpush',
|
||||
regex: /\/settings\/notifications\/webpush/,
|
||||
hidden: !data?.webPushEnabled,
|
||||
},
|
||||
{
|
||||
text: 'Discord',
|
||||
content: (
|
||||
@@ -65,18 +77,6 @@ const UserNotificationSettings: React.FC = ({ children }) => {
|
||||
regex: /\/settings\/notifications\/telegram/,
|
||||
hidden: !data?.telegramEnabled || !data?.telegramBotUsername,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.webpush),
|
||||
content: (
|
||||
<span className="flex items-center">
|
||||
<CloudIcon className="h-4 mr-2" />
|
||||
{intl.formatMessage(messages.webpush)}
|
||||
</span>
|
||||
),
|
||||
route: '/settings/notifications/webpush',
|
||||
regex: /\/settings\/notifications\/webpush/,
|
||||
hidden: !data?.webPushEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
settingsRoutes.forEach((settingsRoute) => {
|
||||
|
@@ -65,6 +65,8 @@ const UserSettings: React.FC = ({ children }) => {
|
||||
text: intl.formatMessage(messages.menuNotifications),
|
||||
route: data?.emailEnabled
|
||||
? '/settings/notifications/email'
|
||||
: data?.webPushEnabled
|
||||
? '/settings/notifications/webpush'
|
||||
: '/settings/notifications/discord',
|
||||
regex: /\/settings\/notifications/,
|
||||
},
|
||||
|
@@ -89,18 +89,24 @@
|
||||
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
||||
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
||||
"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.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.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.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.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.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.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.adminDescription": "Full administrator access. Bypasses all other permission checks.",
|
||||
"components.PermissionEdit.advancedrequest": "Advanced Requests",
|
||||
@@ -261,6 +267,7 @@
|
||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestFailed": "LunaSea test notification failed to send.",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSending": "Sending LunaSea test notification…",
|
||||
"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.webhookUrl": "Webhook URL",
|
||||
"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.toastPushbulletTestSuccess": "Pushbullet test notification sent!",
|
||||
"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.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
|
||||
@@ -284,13 +292,15 @@
|
||||
"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.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.slacksettingsfailed": "Slack notification settings failed to save.",
|
||||
"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.toastSlackTestSending": "Sending Slack test notification…",
|
||||
"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.webhookUrl": "Webhook URL",
|
||||
"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.toastWebhookTestSuccess": "Webhook test notification sent!",
|
||||
"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.webhookUrl": "Webhook URL",
|
||||
"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.discordsettingsfailed": "Discord notification settings failed to save.",
|
||||
"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.emailsettingsfailed": "Email notification settings failed to save.",
|
||||
"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.validationChatIdRequired": "You must provide a valid chat ID",
|
||||
"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.validationPgpPrivateKey": "You must provide a valid PGP private key if a PGP password is entered",
|
||||
"components.Settings.Notifications.validationPgpPassword": "You must provide a PGP password",
|
||||
"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.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.webhookUrl": "Webhook URL",
|
||||
"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.emailsettingsfailed": "Email notification settings failed to save.",
|
||||
"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.notificationsettings": "Notification Settings",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key",
|
||||
|
Reference in New Issue
Block a user