feat(notif): allow users to enable/disable specific agents (#1172)
* refactor(ui): add tabs to user notification settings * feat(notif): allow users to enable/disable specific agents * fix(ui): only enforce required fields when agent is enabled * fix(ui): hide unavailable notification agents * feat(notif): mention admin users for admin Discord notifications * fix(ui): modify styling of PGP key textareas to suit expected input * fix(notif): mention all admins when there are multiple and fix rebase error * fix: add missing form values, and fix Yup validation * refactor: reduce repeated logic/code in email notif agent * refactor: move 'Notification Types' label into NotificationTypeSelector component * fix(email): correct inconsistencies in email template formatting * refactor: use bitfields for storing user-enabled notif agent types * feat: improve notification agent logging * fix(ui): mark string fields as nullable so empty values are not type errors * fix: add validation for PGP-related inputs * fix: correctly fetch user in user settings & log mentioned IDs for Discord notifs * fix(ui): fix mobile nav dropdown text & add hover effect to button-style tabs * fix(notif): process admin email notifications asynchronously * fix(logging): log name of notification type instead of its enum value * fix: mark required fields and pass all user settings values to API * fix(frontend): call mutate after changing email/Discord/Telegram global notif settings * refactor: get global notif settings from relevant API endpoints instead of adding to public settings * fix(notif): fall back to email notifications being enabled (default) if user settings do not exist * fix(notif): do not set notifyUser for MEDIA_PENDING or MEDIA_AUTO_APPROVED * fix: expose notif enabled settings in user notif endpoints & remove global enable notif setting * fix(notif): remove unnecessary allowed_mentions object from Discord payload * fix(notif): use form values for email test notification * fix: make suggested changes and regenerate DB migration * fix: loosen validation of PGP keys * fix: fix user profile settings routes * fix: remove route guard from profile pages
This commit is contained in:
@@ -92,17 +92,12 @@ components:
|
|||||||
UserSettings:
|
UserSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
enableNotifications:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
discordId:
|
discordId:
|
||||||
type: string
|
type: string
|
||||||
telegramChatId:
|
region:
|
||||||
|
type: string
|
||||||
|
language:
|
||||||
type: string
|
type: string
|
||||||
telegramSendSilently:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- enableNotifications
|
|
||||||
MainSettings:
|
MainSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1201,12 +1196,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
priority:
|
priority:
|
||||||
type: number
|
type: number
|
||||||
NotificationSettings:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
enabled:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
NotificationEmailSettings:
|
NotificationEmailSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1559,20 +1548,30 @@ components:
|
|||||||
UserSettingsNotifications:
|
UserSettingsNotifications:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
enableNotifications:
|
notificationAgents:
|
||||||
|
type: number
|
||||||
|
example: 0
|
||||||
|
emailEnabled:
|
||||||
|
type: boolean
|
||||||
|
pgpKey:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
discordEnabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
|
||||||
discordId:
|
discordId:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
|
telegramEnabled:
|
||||||
|
type: boolean
|
||||||
|
telegramBotUsername:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
telegramChatId:
|
telegramChatId:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
telegramSendSilently:
|
telegramSendSilently:
|
||||||
type: boolean
|
type: boolean
|
||||||
nullable: true
|
nullable: true
|
||||||
required:
|
|
||||||
- enableNotifications
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
@@ -2306,37 +2305,6 @@ paths:
|
|||||||
timestamp:
|
timestamp:
|
||||||
type: string
|
type: string
|
||||||
example: 2020-12-15T16:20:00.069Z
|
example: 2020-12-15T16:20:00.069Z
|
||||||
/settings/notifications:
|
|
||||||
get:
|
|
||||||
summary: Return notification settings
|
|
||||||
description: Returns current notification settings in a JSON object.
|
|
||||||
tags:
|
|
||||||
- settings
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Returned settings
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/NotificationSettings'
|
|
||||||
post:
|
|
||||||
summary: Update notification settings
|
|
||||||
description: Updates notification settings with the provided values.
|
|
||||||
tags:
|
|
||||||
- settings
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/NotificationSettings'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 'Values were sucessfully updated'
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/NotificationSettings'
|
|
||||||
/settings/notifications/email:
|
/settings/notifications/email:
|
||||||
get:
|
get:
|
||||||
summary: Get email notification settings
|
summary: Get email notification settings
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ export class MediaRequest {
|
|||||||
subject: movie.title,
|
subject: movie.title,
|
||||||
message: movie.overview,
|
message: movie.overview,
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
notifyUser: this.requestedBy,
|
|
||||||
media,
|
media,
|
||||||
request: this,
|
request: this,
|
||||||
});
|
});
|
||||||
@@ -157,7 +156,6 @@ export class MediaRequest {
|
|||||||
subject: tv.name,
|
subject: tv.name,
|
||||||
message: tv.overview,
|
message: tv.overview,
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||||
notifyUser: this.requestedBy,
|
|
||||||
media,
|
media,
|
||||||
extra: [
|
extra: [
|
||||||
{
|
{
|
||||||
@@ -232,7 +230,7 @@ export class MediaRequest {
|
|||||||
subject: tv.name,
|
subject: tv.name,
|
||||||
message: tv.overview,
|
message: tv.overview,
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||||
notifyUser: this.requestedBy,
|
notifyUser: autoApproved ? undefined : this.requestedBy,
|
||||||
media,
|
media,
|
||||||
extra: [
|
extra: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ export class User {
|
|||||||
logger.info(`Sending generated password email for ${this.email}`, {
|
logger.info(`Sending generated password email for ${this.email}`, {
|
||||||
label: 'User Management',
|
label: 'User Management',
|
||||||
});
|
});
|
||||||
const email = new PreparedEmail();
|
|
||||||
|
const email = new PreparedEmail(getSettings().notifications.agents.email);
|
||||||
await email.send({
|
await email.send({
|
||||||
template: path.join(__dirname, '../templates/email/generatedpassword'),
|
template: path.join(__dirname, '../templates/email/generatedpassword'),
|
||||||
message: {
|
message: {
|
||||||
@@ -193,7 +194,7 @@ export class User {
|
|||||||
logger.info(`Sending reset password email for ${this.email}`, {
|
logger.info(`Sending reset password email for ${this.email}`, {
|
||||||
label: 'User Management',
|
label: 'User Management',
|
||||||
});
|
});
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(getSettings().notifications.agents.email);
|
||||||
await email.send({
|
await email.send({
|
||||||
template: path.join(__dirname, '../templates/email/resetpassword'),
|
template: path.join(__dirname, '../templates/email/resetpassword'),
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {
|
|||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import {
|
||||||
|
hasNotificationAgentEnabled,
|
||||||
|
NotificationAgentType,
|
||||||
|
} from '../lib/notifications/agenttypes';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -20,8 +24,17 @@ export class UserSettings {
|
|||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@Column({ default: true })
|
@Column({ nullable: true })
|
||||||
public enableNotifications: boolean;
|
public region?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public originalLanguage?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'integer', default: NotificationAgentType.EMAIL })
|
||||||
|
public notificationAgents = NotificationAgentType.EMAIL;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public pgpKey?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public discordId?: string;
|
public discordId?: string;
|
||||||
@@ -32,12 +45,7 @@ export class UserSettings {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public telegramSendSilently?: boolean;
|
public telegramSendSilently?: boolean;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
public hasNotificationAgentEnabled(agent: NotificationAgentType): boolean {
|
||||||
public region?: string;
|
return !!hasNotificationAgentEnabled(agent, this.notificationAgents);
|
||||||
|
}
|
||||||
@Column({ nullable: true })
|
|
||||||
public originalLanguage?: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
public pgpKey?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ export interface UserSettingsGeneralResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSettingsNotificationsResponse {
|
export interface UserSettingsNotificationsResponse {
|
||||||
enableNotifications: boolean;
|
notificationAgents: number;
|
||||||
telegramBotUsername?: string;
|
emailEnabled?: boolean;
|
||||||
|
pgpKey?: string;
|
||||||
|
discordEnabled?: boolean;
|
||||||
discordId?: string;
|
discordId?: string;
|
||||||
|
telegramEnabled?: boolean;
|
||||||
|
telegramBotUsername?: string;
|
||||||
telegramChatId?: string;
|
telegramChatId?: string;
|
||||||
telegramSendSilently?: boolean;
|
telegramSendSilently?: boolean;
|
||||||
pgpKey?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import nodemailer from 'nodemailer';
|
|
||||||
import Email from 'email-templates';
|
import Email from 'email-templates';
|
||||||
import { getSettings } from '../settings';
|
import nodemailer from 'nodemailer';
|
||||||
|
import { NotificationAgentEmail } from '../settings';
|
||||||
import { openpgpEncrypt } from './openpgpEncrypt';
|
import { openpgpEncrypt } from './openpgpEncrypt';
|
||||||
class PreparedEmail extends Email {
|
|
||||||
public constructor(pgpKey?: string) {
|
|
||||||
const settings = getSettings().notifications.agents.email;
|
|
||||||
|
|
||||||
|
class PreparedEmail extends Email {
|
||||||
|
public constructor(settings: NotificationAgentEmail, pgpKey?: string) {
|
||||||
const transport = nodemailer.createTransport({
|
const transport = nodemailer.createTransport({
|
||||||
host: settings.options.smtpHost,
|
host: settings.options.smtpHost,
|
||||||
port: settings.options.smtpPort,
|
port: settings.options.smtpPort,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { getRepository } from 'typeorm';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
|
import { User } from '../../../entity/User';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
|
import { Permission } from '../../permissions';
|
||||||
import { getSettings, NotificationAgentDiscord } from '../../settings';
|
import { getSettings, NotificationAgentDiscord } from '../../settings';
|
||||||
|
import { NotificationAgentType } from '../agenttypes';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
|
|
||||||
enum EmbedColors {
|
enum EmbedColors {
|
||||||
@@ -107,7 +111,7 @@ class DiscordAgent
|
|||||||
if (payload.request) {
|
if (payload.request) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: 'Requested By',
|
name: 'Requested By',
|
||||||
value: payload.request?.requestedBy.displayName ?? '',
|
value: payload.request.requestedBy.displayName,
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,7 +205,14 @@ class DiscordAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending Discord notification', { label: 'Notifications' });
|
logger.debug('Sending Discord notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
let content = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
botUsername,
|
botUsername,
|
||||||
@@ -213,35 +224,52 @@ class DiscordAgent
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentionedUsers: string[] = [];
|
if (payload.notifyUser) {
|
||||||
let content = undefined;
|
// Mention user who submitted the request
|
||||||
|
|
||||||
if (
|
if (
|
||||||
payload.notifyUser &&
|
payload.notifyUser.settings?.hasNotificationAgentEnabled(
|
||||||
(payload.notifyUser.settings?.enableNotifications ?? true) &&
|
NotificationAgentType.DISCORD
|
||||||
|
) &&
|
||||||
payload.notifyUser.settings?.discordId
|
payload.notifyUser.settings?.discordId
|
||||||
) {
|
) {
|
||||||
mentionedUsers.push(payload.notifyUser.settings.discordId);
|
|
||||||
content = `<@${payload.notifyUser.settings.discordId}>`;
|
content = `<@${payload.notifyUser.settings.discordId}>`;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Mention all users with the Manage Requests permission
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const users = await userRepository.find();
|
||||||
|
|
||||||
|
content = users
|
||||||
|
.filter(
|
||||||
|
(user) =>
|
||||||
|
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||||
|
user.settings?.hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.DISCORD
|
||||||
|
) &&
|
||||||
|
user.settings?.discordId
|
||||||
|
)
|
||||||
|
.map((user) => `<@${user.settings?.discordId}>`)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
await axios.post(webhookUrl, {
|
await axios.post(webhookUrl, {
|
||||||
username: botUsername,
|
username: botUsername,
|
||||||
avatar_url: botAvatarUrl,
|
avatar_url: botAvatarUrl,
|
||||||
embeds: [this.buildEmbed(type, payload)],
|
embeds: [this.buildEmbed(type, payload)],
|
||||||
content,
|
content,
|
||||||
allowed_mentions: {
|
|
||||||
users: mentionedUsers,
|
|
||||||
},
|
|
||||||
} as DiscordWebhookPayload);
|
} as DiscordWebhookPayload);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending Discord notification', {
|
logger.error('Error sending Discord notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
message: e.message,
|
mentions: content,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
response: e.response.data,
|
response: e.response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { EmailOptions } from 'email-templates';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
@@ -7,6 +8,7 @@ import logger from '../../../logger';
|
|||||||
import PreparedEmail from '../../email';
|
import PreparedEmail from '../../email';
|
||||||
import { Permission } from '../../permissions';
|
import { Permission } from '../../permissions';
|
||||||
import { getSettings, NotificationAgentEmail } from '../../settings';
|
import { getSettings, NotificationAgentEmail } from '../../settings';
|
||||||
|
import { NotificationAgentType } from '../agenttypes';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
|
|
||||||
class EmailAgent
|
class EmailAgent
|
||||||
@@ -35,379 +37,194 @@ class EmailAgent
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendMediaRequestEmail(payload: NotificationPayload) {
|
private buildMessage(
|
||||||
// This is getting main settings for the whole app
|
type: Notification,
|
||||||
|
payload: NotificationPayload,
|
||||||
|
toEmail: string
|
||||||
|
): EmailOptions | undefined {
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||||
try {
|
|
||||||
const userRepository = getRepository(User);
|
|
||||||
const users = await userRepository.find();
|
|
||||||
|
|
||||||
// Send to all users with the manage requests permission (or admins)
|
if (type === Notification.TEST_NOTIFICATION) {
|
||||||
users
|
return {
|
||||||
.filter(
|
|
||||||
(user) =>
|
|
||||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
|
||||||
(user.settings?.enableNotifications ?? true)
|
|
||||||
)
|
|
||||||
.forEach((user) => {
|
|
||||||
const email = new PreparedEmail(user.settings?.pgpKey);
|
|
||||||
|
|
||||||
email.send({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../templates/email/media-request'
|
|
||||||
),
|
|
||||||
message: {
|
|
||||||
to: user.email,
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
body: `A user has requested a new ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
|
||||||
}!`,
|
|
||||||
mediaName: payload.subject,
|
|
||||||
mediaPlot: payload.message,
|
|
||||||
mediaExtra: payload.extra ?? [],
|
|
||||||
imageUrl: payload.image,
|
|
||||||
timestamp: new Date().toTimeString(),
|
|
||||||
requestedBy: payload.request?.requestedBy.displayName,
|
|
||||||
actionUrl: applicationUrl
|
|
||||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
||||||
: undefined,
|
|
||||||
applicationUrl,
|
|
||||||
applicationTitle,
|
|
||||||
requestType: `New ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
|
||||||
} Request`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Email notification failed to send', {
|
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendMediaFailedEmail(payload: NotificationPayload) {
|
|
||||||
// This is getting main settings for the whole app
|
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
|
||||||
try {
|
|
||||||
const userRepository = getRepository(User);
|
|
||||||
const users = await userRepository.find();
|
|
||||||
|
|
||||||
// Send to all users with the manage requests permission (or admins)
|
|
||||||
users
|
|
||||||
.filter(
|
|
||||||
(user) =>
|
|
||||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
|
||||||
(user.settings?.enableNotifications ?? true)
|
|
||||||
)
|
|
||||||
.forEach((user) => {
|
|
||||||
const email = new PreparedEmail(user.settings?.pgpKey);
|
|
||||||
|
|
||||||
email.send({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../templates/email/media-request'
|
|
||||||
),
|
|
||||||
message: {
|
|
||||||
to: user.email,
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
body: `A new request for the following ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
|
||||||
} could not be added to ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Sonarr' : 'Radarr'
|
|
||||||
}:`,
|
|
||||||
mediaName: payload.subject,
|
|
||||||
mediaPlot: payload.message,
|
|
||||||
imageUrl: payload.image,
|
|
||||||
timestamp: new Date().toTimeString(),
|
|
||||||
requestedBy: payload.request?.requestedBy.displayName,
|
|
||||||
actionUrl: applicationUrl
|
|
||||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
||||||
: undefined,
|
|
||||||
applicationUrl,
|
|
||||||
applicationTitle,
|
|
||||||
requestType: `Failed ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
|
||||||
} Request`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Email notification failed to send', {
|
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
|
||||||
// This is getting main settings for the whole app
|
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
payload.notifyUser &&
|
|
||||||
(payload.notifyUser.settings?.enableNotifications ?? true)
|
|
||||||
) {
|
|
||||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
|
||||||
|
|
||||||
await email.send({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../templates/email/media-request'
|
|
||||||
),
|
|
||||||
message: {
|
|
||||||
to: payload.notifyUser.email,
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
body: `Your request for the following ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
|
||||||
} has been approved:`,
|
|
||||||
mediaName: payload.subject,
|
|
||||||
mediaExtra: payload.extra ?? [],
|
|
||||||
imageUrl: payload.image,
|
|
||||||
timestamp: new Date().toTimeString(),
|
|
||||||
requestedBy: payload.request?.requestedBy.displayName,
|
|
||||||
actionUrl: applicationUrl
|
|
||||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
||||||
: undefined,
|
|
||||||
applicationUrl,
|
|
||||||
applicationTitle,
|
|
||||||
requestType: `${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
|
||||||
} Request Approved`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Email notification failed to send', {
|
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendMediaAutoApprovedEmail(payload: NotificationPayload) {
|
|
||||||
// This is getting main settings for the whole app
|
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
|
||||||
try {
|
|
||||||
const userRepository = getRepository(User);
|
|
||||||
const users = await userRepository.find();
|
|
||||||
|
|
||||||
// Send to all users with the manage requests permission (or admins)
|
|
||||||
users
|
|
||||||
.filter(
|
|
||||||
(user) =>
|
|
||||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
|
||||||
(user.settings?.enableNotifications ?? true)
|
|
||||||
)
|
|
||||||
.forEach((user) => {
|
|
||||||
const email = new PreparedEmail();
|
|
||||||
|
|
||||||
email.send({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../templates/email/media-request'
|
|
||||||
),
|
|
||||||
message: {
|
|
||||||
to: user.email,
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
body: `A new request for the following ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
|
||||||
} has been automatically approved:`,
|
|
||||||
mediaName: payload.subject,
|
|
||||||
mediaExtra: payload.extra ?? [],
|
|
||||||
imageUrl: payload.image,
|
|
||||||
timestamp: new Date().toTimeString(),
|
|
||||||
requestedBy: payload.request?.requestedBy.displayName,
|
|
||||||
actionUrl: applicationUrl
|
|
||||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
||||||
: undefined,
|
|
||||||
applicationUrl,
|
|
||||||
applicationTitle,
|
|
||||||
requestType: `${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
|
||||||
} Request Automatically Approved`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Email notification failed to send', {
|
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendMediaDeclinedEmail(payload: NotificationPayload) {
|
|
||||||
// This is getting main settings for the whole app
|
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
payload.notifyUser &&
|
|
||||||
(payload.notifyUser.settings?.enableNotifications ?? true)
|
|
||||||
) {
|
|
||||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
|
||||||
|
|
||||||
await email.send({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../templates/email/media-request'
|
|
||||||
),
|
|
||||||
message: {
|
|
||||||
to: payload.notifyUser.email,
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
body: `Your request for the following ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
|
||||||
} was declined:`,
|
|
||||||
mediaName: payload.subject,
|
|
||||||
mediaExtra: payload.extra ?? [],
|
|
||||||
imageUrl: payload.image,
|
|
||||||
timestamp: new Date().toTimeString(),
|
|
||||||
requestedBy: payload.request?.requestedBy.displayName,
|
|
||||||
actionUrl: applicationUrl
|
|
||||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
||||||
: undefined,
|
|
||||||
applicationUrl,
|
|
||||||
applicationTitle,
|
|
||||||
requestType: `${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
|
||||||
} Request Declined`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Email notification failed to send', {
|
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendMediaAvailableEmail(payload: NotificationPayload) {
|
|
||||||
// This is getting main settings for the whole app
|
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
payload.notifyUser &&
|
|
||||||
(payload.notifyUser.settings?.enableNotifications ?? true)
|
|
||||||
) {
|
|
||||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
|
||||||
|
|
||||||
await email.send({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../templates/email/media-request'
|
|
||||||
),
|
|
||||||
message: {
|
|
||||||
to: payload.notifyUser.email,
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
body: `The following ${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
|
||||||
} you requested is now available!`,
|
|
||||||
mediaName: payload.subject,
|
|
||||||
mediaExtra: payload.extra ?? [],
|
|
||||||
imageUrl: payload.image,
|
|
||||||
timestamp: new Date().toTimeString(),
|
|
||||||
requestedBy: payload.request?.requestedBy.displayName,
|
|
||||||
actionUrl: applicationUrl
|
|
||||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
||||||
: undefined,
|
|
||||||
applicationUrl,
|
|
||||||
applicationTitle,
|
|
||||||
requestType: `${
|
|
||||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
|
||||||
} Now Available`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Email notification failed to send', {
|
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendTestEmail(payload: NotificationPayload) {
|
|
||||||
// This is getting main settings for the whole app
|
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
|
||||||
try {
|
|
||||||
if (payload.notifyUser) {
|
|
||||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
|
||||||
|
|
||||||
await email.send({
|
|
||||||
template: path.join(__dirname, '../../../templates/email/test-email'),
|
template: path.join(__dirname, '../../../templates/email/test-email'),
|
||||||
message: {
|
message: {
|
||||||
to: payload.notifyUser.email,
|
to: toEmail,
|
||||||
},
|
},
|
||||||
locals: {
|
locals: {
|
||||||
body: payload.message,
|
body: payload.message,
|
||||||
applicationUrl,
|
applicationUrl,
|
||||||
applicationTitle,
|
applicationTitle,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (payload.media) {
|
||||||
} catch (e) {
|
let requestType = '';
|
||||||
logger.error('Email notification failed to send', {
|
let body = '';
|
||||||
label: 'Notifications',
|
|
||||||
message: e.message,
|
switch (type) {
|
||||||
});
|
case Notification.MEDIA_PENDING:
|
||||||
return false;
|
requestType = `New ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||||
|
} Request`;
|
||||||
|
body = `A user has requested a new ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||||
|
}!`;
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_APPROVED:
|
||||||
|
requestType = `${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||||
|
} Request Approved`;
|
||||||
|
body = `Your request for the following ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||||
|
} has been approved:`;
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_AUTO_APPROVED:
|
||||||
|
requestType = `${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||||
|
} Request Automatically Approved`;
|
||||||
|
body = `A new request for the following ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||||
|
} has been automatically approved:`;
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_AVAILABLE:
|
||||||
|
requestType = `${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||||
|
} Now Available`;
|
||||||
|
body = `The following ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||||
|
} you requested is now available!`;
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_DECLINED:
|
||||||
|
requestType = `${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||||
|
} Request Declined`;
|
||||||
|
body = `Your request for the following ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||||
|
} was declined:`;
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_FAILED:
|
||||||
|
requestType = `Failed ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||||
|
} Request`;
|
||||||
|
body = `A new request for the following ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||||
|
} could not be added to ${
|
||||||
|
payload.media?.mediaType === MediaType.TV ? 'Sonarr' : 'Radarr'
|
||||||
|
}:`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
template: path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../templates/email/media-request'
|
||||||
|
),
|
||||||
|
message: {
|
||||||
|
to: toEmail,
|
||||||
|
},
|
||||||
|
locals: {
|
||||||
|
requestType,
|
||||||
|
body,
|
||||||
|
mediaName: payload.subject,
|
||||||
|
mediaPlot: payload.message,
|
||||||
|
mediaExtra: payload.extra ?? [],
|
||||||
|
imageUrl: payload.image,
|
||||||
|
timestamp: new Date().toTimeString(),
|
||||||
|
requestedBy: payload.request?.requestedBy.displayName,
|
||||||
|
actionUrl: applicationUrl
|
||||||
|
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||||
|
: undefined,
|
||||||
|
applicationUrl,
|
||||||
|
applicationTitle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(
|
public async send(
|
||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending email notification', { label: 'Notifications' });
|
if (payload.notifyUser) {
|
||||||
|
// Send notification to the user who submitted the request
|
||||||
|
if (
|
||||||
|
!payload.notifyUser.settings ||
|
||||||
|
payload.notifyUser.settings.hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.EMAIL
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
logger.debug('Sending email notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: payload.notifyUser.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
switch (type) {
|
try {
|
||||||
case Notification.MEDIA_PENDING:
|
const email = new PreparedEmail(
|
||||||
this.sendMediaRequestEmail(payload);
|
this.getSettings(),
|
||||||
break;
|
payload.notifyUser.settings?.pgpKey
|
||||||
case Notification.MEDIA_APPROVED:
|
);
|
||||||
this.sendMediaApprovedEmail(payload);
|
await email.send(
|
||||||
break;
|
this.buildMessage(type, payload, payload.notifyUser.email)
|
||||||
case Notification.MEDIA_AUTO_APPROVED:
|
);
|
||||||
this.sendMediaAutoApprovedEmail(payload);
|
} catch (e) {
|
||||||
break;
|
logger.error('Error sending email notification', {
|
||||||
case Notification.MEDIA_DECLINED:
|
label: 'Notifications',
|
||||||
this.sendMediaDeclinedEmail(payload);
|
recipient: payload.notifyUser.displayName,
|
||||||
break;
|
type: Notification[type],
|
||||||
case Notification.MEDIA_AVAILABLE:
|
subject: payload.subject,
|
||||||
this.sendMediaAvailableEmail(payload);
|
errorMessage: e.message,
|
||||||
break;
|
});
|
||||||
case Notification.MEDIA_FAILED:
|
|
||||||
this.sendMediaFailedEmail(payload);
|
return false;
|
||||||
break;
|
}
|
||||||
case Notification.TEST_NOTIFICATION:
|
}
|
||||||
this.sendTestEmail(payload);
|
} else {
|
||||||
break;
|
// 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 ||
|
||||||
|
user.settings.hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.EMAIL
|
||||||
|
))
|
||||||
|
)
|
||||||
|
.map(async (user) => {
|
||||||
|
logger.debug('Sending email notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const email = new PreparedEmail(
|
||||||
|
this.getSettings(),
|
||||||
|
user.settings?.pgpKey
|
||||||
|
);
|
||||||
|
await email.send(this.buildMessage(type, payload, user.email));
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error sending email notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
|
import { MediaType } from '../../../constants/media';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { getSettings, NotificationAgentPushbullet } from '../../settings';
|
import { getSettings, NotificationAgentPushbullet } from '../../settings';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { MediaType } from '../../../constants/media';
|
|
||||||
|
|
||||||
interface PushbulletPayload {
|
interface PushbulletPayload {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -136,7 +136,12 @@ class PushbulletAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending Pushbullet notification', { label: 'Notifications' });
|
logger.debug('Sending Pushbullet notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = 'https://api.pushbullet.com/v2/pushes';
|
const endpoint = 'https://api.pushbullet.com/v2/pushes';
|
||||||
|
|
||||||
@@ -162,8 +167,12 @@ class PushbulletAgent
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending Pushbullet notification', {
|
logger.error('Error sending Pushbullet notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
message: e.message,
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
|
import { MediaType } from '../../../constants/media';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { getSettings, NotificationAgentPushover } from '../../settings';
|
import { getSettings, NotificationAgentPushover } from '../../settings';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { MediaType } from '../../../constants/media';
|
|
||||||
|
|
||||||
interface PushoverPayload {
|
interface PushoverPayload {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -160,7 +160,11 @@ class PushoverAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending Pushover notification', { label: 'Notifications' });
|
logger.debug('Sending Pushover notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const endpoint = 'https://api.pushover.net/1/messages.json';
|
const endpoint = 'https://api.pushover.net/1/messages.json';
|
||||||
|
|
||||||
@@ -189,8 +193,12 @@ class PushoverAgent
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending Pushover notification', {
|
logger.error('Error sending Pushover notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
message: e.message,
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
|
import { MediaType } from '../../../constants/media';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { getSettings, NotificationAgentSlack } from '../../settings';
|
import { getSettings, NotificationAgentSlack } from '../../settings';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { MediaType } from '../../../constants/media';
|
|
||||||
|
|
||||||
interface EmbedField {
|
interface EmbedField {
|
||||||
type: 'plain_text' | 'mrkdwn';
|
type: 'plain_text' | 'mrkdwn';
|
||||||
@@ -67,9 +67,7 @@ class SlackAgent
|
|||||||
if (payload.request) {
|
if (payload.request) {
|
||||||
fields.push({
|
fields.push({
|
||||||
type: 'mrkdwn',
|
type: 'mrkdwn',
|
||||||
text: `*Requested By*\n${
|
text: `*Requested By*\n${payload.request.requestedBy.displayName}`,
|
||||||
payload.request?.requestedBy.displayName ?? ''
|
|
||||||
}`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +233,11 @@ class SlackAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending Slack notification', { label: 'Notifications' });
|
logger.debug('Sending Slack notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const webhookUrl = this.getSettings().options.webhookUrl;
|
const webhookUrl = this.getSettings().options.webhookUrl;
|
||||||
|
|
||||||
@@ -249,8 +251,12 @@ class SlackAgent
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending Slack notification', {
|
logger.error('Error sending Slack notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
message: e.message,
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { hasNotificationType, Notification } from '..';
|
|||||||
import { MediaType } from '../../../constants/media';
|
import { MediaType } from '../../../constants/media';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { getSettings, NotificationAgentTelegram } from '../../settings';
|
import { getSettings, NotificationAgentTelegram } from '../../settings';
|
||||||
|
import { NotificationAgentType } from '../agenttypes';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
|
|
||||||
interface TelegramMessagePayload {
|
interface TelegramMessagePayload {
|
||||||
@@ -155,63 +156,99 @@ class TelegramAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending Telegram notification', { label: 'Notifications' });
|
const endpoint = `${this.baseUrl}bot${this.getSettings().options.botAPI}/${
|
||||||
try {
|
payload.image ? 'sendPhoto' : 'sendMessage'
|
||||||
const endpoint = `${this.baseUrl}bot${
|
}`;
|
||||||
this.getSettings().options.botAPI
|
|
||||||
}/${payload.image ? 'sendPhoto' : 'sendMessage'}`;
|
|
||||||
|
|
||||||
// Send system notification
|
// Send system notification
|
||||||
await (payload.image
|
try {
|
||||||
? axios.post(endpoint, {
|
logger.debug('Sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
endpoint,
|
||||||
|
payload.image
|
||||||
|
? ({
|
||||||
photo: payload.image,
|
photo: payload.image,
|
||||||
caption: this.buildMessage(type, payload),
|
caption: this.buildMessage(type, payload),
|
||||||
parse_mode: 'MarkdownV2',
|
parse_mode: 'MarkdownV2',
|
||||||
chat_id: `${this.getSettings().options.chatId}`,
|
chat_id: this.getSettings().options.chatId,
|
||||||
disable_notification: this.getSettings().options.sendSilently,
|
disable_notification: this.getSettings().options.sendSilently,
|
||||||
} as TelegramPhotoPayload)
|
} as TelegramPhotoPayload)
|
||||||
: axios.post(endpoint, {
|
: ({
|
||||||
text: this.buildMessage(type, payload),
|
text: this.buildMessage(type, payload),
|
||||||
parse_mode: 'MarkdownV2',
|
parse_mode: 'MarkdownV2',
|
||||||
chat_id: `${this.getSettings().options.chatId}`,
|
chat_id: `${this.getSettings().options.chatId}`,
|
||||||
disable_notification: this.getSettings().options.sendSilently,
|
disable_notification: this.getSettings().options.sendSilently,
|
||||||
} as TelegramMessagePayload));
|
} as TelegramMessagePayload)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response.data,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Send user notification
|
|
||||||
if (
|
if (
|
||||||
payload.notifyUser &&
|
payload.notifyUser &&
|
||||||
(payload.notifyUser.settings?.enableNotifications ?? true) &&
|
payload.notifyUser.settings?.hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.TELEGRAM
|
||||||
|
) &&
|
||||||
payload.notifyUser.settings?.telegramChatId &&
|
payload.notifyUser.settings?.telegramChatId &&
|
||||||
payload.notifyUser.settings?.telegramChatId !==
|
payload.notifyUser.settings?.telegramChatId !==
|
||||||
this.getSettings().options.chatId
|
this.getSettings().options.chatId
|
||||||
) {
|
) {
|
||||||
await (payload.image
|
// Send notification to the user who submitted the request
|
||||||
? axios.post(endpoint, {
|
logger.debug('Sending Telegram notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: payload.notifyUser.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
endpoint,
|
||||||
|
payload.image
|
||||||
|
? ({
|
||||||
photo: payload.image,
|
photo: payload.image,
|
||||||
caption: this.buildMessage(type, payload),
|
caption: this.buildMessage(type, payload),
|
||||||
parse_mode: 'MarkdownV2',
|
parse_mode: 'MarkdownV2',
|
||||||
chat_id: `${payload.notifyUser.settings.telegramChatId}`,
|
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||||
disable_notification:
|
disable_notification:
|
||||||
payload.notifyUser.settings.telegramSendSilently,
|
payload.notifyUser.settings.telegramSendSilently,
|
||||||
} as TelegramPhotoPayload)
|
} as TelegramPhotoPayload)
|
||||||
: axios.post(endpoint, {
|
: ({
|
||||||
text: this.buildMessage(type, payload),
|
text: this.buildMessage(type, payload),
|
||||||
parse_mode: 'MarkdownV2',
|
parse_mode: 'MarkdownV2',
|
||||||
chat_id: `${payload.notifyUser.settings.telegramChatId}`,
|
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||||
disable_notification:
|
disable_notification:
|
||||||
payload.notifyUser.settings.telegramSendSilently,
|
payload.notifyUser.settings.telegramSendSilently,
|
||||||
} as TelegramMessagePayload));
|
} as TelegramMessagePayload)
|
||||||
}
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending Telegram notification', {
|
logger.error('Error sending Telegram notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
message: e.message,
|
recipient: payload.notifyUser.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
response: e.response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TelegramAgent;
|
export default TelegramAgent;
|
||||||
|
|||||||
@@ -128,7 +128,12 @@ class WebhookAgent
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.debug('Sending webhook notification', { label: 'Notifications' });
|
logger.debug('Sending webhook notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webhookUrl, authHeader } = this.getSettings().options;
|
const { webhookUrl, authHeader } = this.getSettings().options;
|
||||||
|
|
||||||
@@ -146,8 +151,12 @@ class WebhookAgent
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending webhook notification', {
|
logger.error('Error sending webhook notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
|
response: e.response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
server/lib/notifications/agenttypes.ts
Normal file
16
server/lib/notifications/agenttypes.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export enum NotificationAgentType {
|
||||||
|
NONE = 0,
|
||||||
|
EMAIL = 2,
|
||||||
|
DISCORD = 4,
|
||||||
|
TELEGRAM = 8,
|
||||||
|
PUSHOVER = 16,
|
||||||
|
PUSHBULLET = 32,
|
||||||
|
SLACK = 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasNotificationAgentEnabled = (
|
||||||
|
agent: NotificationAgentType,
|
||||||
|
value: number
|
||||||
|
): boolean => {
|
||||||
|
return !!(value & agent);
|
||||||
|
};
|
||||||
@@ -38,7 +38,7 @@ class NotificationManager {
|
|||||||
|
|
||||||
public registerAgents = (agents: NotificationAgent[]): void => {
|
public registerAgents = (agents: NotificationAgent[]): void => {
|
||||||
this.activeAgents = [...this.activeAgents, ...agents];
|
this.activeAgents = [...this.activeAgents, ...agents];
|
||||||
logger.info('Registered Notification Agents', { label: 'Notifications' });
|
logger.info('Registered notification agents', { label: 'Notifications' });
|
||||||
};
|
};
|
||||||
|
|
||||||
public sendNotification(
|
public sendNotification(
|
||||||
@@ -46,8 +46,9 @@ class NotificationManager {
|
|||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): void {
|
): void {
|
||||||
const settings = getSettings().notifications;
|
const settings = getSettings().notifications;
|
||||||
logger.info(`Sending notification for ${Notification[type]}`, {
|
logger.info(`Sending notification(s) for ${Notification[type]}`, {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
|
subject: payload.subject,
|
||||||
});
|
});
|
||||||
this.activeAgents.forEach((agent) => {
|
this.activeAgents.forEach((agent) => {
|
||||||
if (settings.enabled && agent.shouldSend(type)) {
|
if (settings.enabled && agent.shouldSend(type)) {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddUserSettingsNotificationAgentsField1617730837489
|
||||||
|
implements MigrationInterface {
|
||||||
|
name = 'AddUserSettingsNotificationAgentsField1617730837489';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_settings"("id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_settings"("id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,16 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getSettings } from '../../lib/settings';
|
|
||||||
import { Notification } from '../../lib/notifications';
|
import { Notification } from '../../lib/notifications';
|
||||||
import DiscordAgent from '../../lib/notifications/agents/discord';
|
import DiscordAgent from '../../lib/notifications/agents/discord';
|
||||||
import EmailAgent from '../../lib/notifications/agents/email';
|
import EmailAgent from '../../lib/notifications/agents/email';
|
||||||
|
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
|
||||||
|
import PushoverAgent from '../../lib/notifications/agents/pushover';
|
||||||
import SlackAgent from '../../lib/notifications/agents/slack';
|
import SlackAgent from '../../lib/notifications/agents/slack';
|
||||||
import TelegramAgent from '../../lib/notifications/agents/telegram';
|
import TelegramAgent from '../../lib/notifications/agents/telegram';
|
||||||
import PushoverAgent from '../../lib/notifications/agents/pushover';
|
|
||||||
import WebhookAgent from '../../lib/notifications/agents/webhook';
|
import WebhookAgent from '../../lib/notifications/agents/webhook';
|
||||||
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
|
import { getSettings } from '../../lib/settings';
|
||||||
|
|
||||||
const notificationRoutes = Router();
|
const notificationRoutes = Router();
|
||||||
|
|
||||||
notificationRoutes.get('/', (_req, res) => {
|
|
||||||
const settings = getSettings().notifications;
|
|
||||||
return res.status(200).json({
|
|
||||||
enabled: settings.enabled,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationRoutes.post('/', (req, res) => {
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
Object.assign(settings.notifications, {
|
|
||||||
enabled: req.body.enabled,
|
|
||||||
});
|
|
||||||
settings.save();
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
enabled: settings.notifications.enabled,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationRoutes.get('/discord', (_req, res) => {
|
notificationRoutes.get('/discord', (_req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
UserSettingsGeneralResponse,
|
UserSettingsGeneralResponse,
|
||||||
UserSettingsNotificationsResponse,
|
UserSettingsNotificationsResponse,
|
||||||
} from '../../interfaces/api/userSettingsInterfaces';
|
} from '../../interfaces/api/userSettingsInterfaces';
|
||||||
|
import { NotificationAgentType } from '../../lib/notifications/agenttypes';
|
||||||
import { Permission } from '../../lib/permissions';
|
import { Permission } from '../../lib/permissions';
|
||||||
import { getSettings } from '../../lib/settings';
|
import { getSettings } from '../../lib/settings';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
@@ -242,13 +243,17 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
enableNotifications: user.settings?.enableNotifications ?? true,
|
notificationAgents:
|
||||||
|
user.settings?.notificationAgents ?? NotificationAgentType.EMAIL,
|
||||||
|
emailEnabled: settings?.notifications.agents.email.enabled,
|
||||||
|
pgpKey: user.settings?.pgpKey,
|
||||||
|
discordEnabled: settings?.notifications.agents.discord.enabled,
|
||||||
|
discordId: user.settings?.discordId,
|
||||||
|
telegramEnabled: settings?.notifications.agents.telegram.enabled,
|
||||||
telegramBotUsername:
|
telegramBotUsername:
|
||||||
settings?.notifications.agents.telegram.options.botUsername,
|
settings?.notifications.agents.telegram.options.botUsername,
|
||||||
discordId: user.settings?.discordId,
|
|
||||||
telegramChatId: user.settings?.telegramChatId,
|
telegramChatId: user.settings?.telegramChatId,
|
||||||
telegramSendSilently: user?.settings?.telegramSendSilently,
|
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||||
pgpKey: user?.settings?.pgpKey,
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
@@ -256,11 +261,10 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
userSettingsRoutes.post<
|
userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
|
||||||
{ id: string },
|
'/notifications',
|
||||||
UserSettingsNotificationsResponse,
|
isOwnProfileOrAdmin(),
|
||||||
UserSettingsNotificationsResponse
|
async (req, res, next) => {
|
||||||
>('/notifications', isOwnProfileOrAdmin(), async (req, res, next) => {
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -283,33 +287,36 @@ userSettingsRoutes.post<
|
|||||||
if (!user.settings) {
|
if (!user.settings) {
|
||||||
user.settings = new UserSettings({
|
user.settings = new UserSettings({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
enableNotifications: req.body.enableNotifications,
|
notificationAgents:
|
||||||
|
req.body.notificationAgents ?? NotificationAgentType.EMAIL,
|
||||||
|
pgpKey: req.body.pgpKey,
|
||||||
discordId: req.body.discordId,
|
discordId: req.body.discordId,
|
||||||
telegramChatId: req.body.telegramChatId,
|
telegramChatId: req.body.telegramChatId,
|
||||||
telegramSendSilently: req.body.telegramSendSilently,
|
telegramSendSilently: req.body.telegramSendSilently,
|
||||||
pgpKey: req.body.pgpKey,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
user.settings.enableNotifications = req.body.enableNotifications;
|
user.settings.notificationAgents =
|
||||||
|
req.body.notificationAgents ?? NotificationAgentType.EMAIL;
|
||||||
|
user.settings.pgpKey = req.body.pgpKey;
|
||||||
user.settings.discordId = req.body.discordId;
|
user.settings.discordId = req.body.discordId;
|
||||||
user.settings.telegramChatId = req.body.telegramChatId;
|
user.settings.telegramChatId = req.body.telegramChatId;
|
||||||
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
||||||
user.settings.pgpKey = req.body.pgpKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
enableNotifications: user.settings.enableNotifications,
|
notificationAgents: user.settings?.notificationAgents,
|
||||||
discordId: user.settings.discordId,
|
pgpKey: user.settings?.pgpKey,
|
||||||
telegramChatId: user.settings.telegramChatId,
|
discordId: user.settings?.discordId,
|
||||||
telegramSendSilently: user.settings.telegramSendSilently,
|
telegramChatId: user.settings?.telegramChatId,
|
||||||
pgpKey: user.settings.pgpKey,
|
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
userSettingsRoutes.get<{ id: string }, { permissions?: number }>(
|
userSettingsRoutes.get<{ id: string }, { permissions?: number }>(
|
||||||
'/permissions',
|
'/permissions',
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
|||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(align='center' style='\
|
||||||
font-size: 16px;\
|
|
||||||
padding-top: 25px;\
|
padding-top: 25px;\
|
||||||
padding-bottom: 25px;\
|
padding-bottom: 25px;\
|
||||||
text-align: center;\
|
text-align: center;\
|
||||||
@@ -50,7 +49,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
|||||||
a(href=applicationUrl style='\
|
a(href=applicationUrl style='\
|
||||||
text-shadow: 0 1px 0 #ffffff;\
|
text-shadow: 0 1px 0 #ffffff;\
|
||||||
font-weight: 700;\
|
font-weight: 700;\
|
||||||
font-size: 16px;\
|
font-size: 24px;\
|
||||||
color: #a8aaaf;\
|
color: #a8aaaf;\
|
||||||
text-decoration: none;\
|
text-decoration: none;\
|
||||||
')
|
')
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
|||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(align='center' style='\
|
||||||
font-size: 16px;\
|
|
||||||
padding-top: 25px;\
|
padding-top: 25px;\
|
||||||
padding-bottom: 25px;\
|
padding-bottom: 25px;\
|
||||||
text-align: center;\
|
text-align: center;\
|
||||||
@@ -50,7 +49,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
|||||||
a(href=applicationUrl style='\
|
a(href=applicationUrl style='\
|
||||||
text-shadow: 0 1px 0 #ffffff;\
|
text-shadow: 0 1px 0 #ffffff;\
|
||||||
font-weight: 700;\
|
font-weight: 700;\
|
||||||
font-size: 16px;\
|
font-size: 24px;\
|
||||||
color: #a8aaaf;\
|
color: #a8aaaf;\
|
||||||
text-decoration: none;\
|
text-decoration: none;\
|
||||||
')
|
')
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
|||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(align='center' style='\
|
||||||
font-size: 16px;\
|
|
||||||
padding-top: 25px;\
|
padding-top: 25px;\
|
||||||
padding-bottom: 25px;\
|
padding-bottom: 25px;\
|
||||||
text-align: center;\
|
text-align: center;\
|
||||||
@@ -50,7 +49,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
|||||||
a(href=applicationUrl style='\
|
a(href=applicationUrl style='\
|
||||||
text-shadow: 0 1px 0 #ffffff;\
|
text-shadow: 0 1px 0 #ffffff;\
|
||||||
font-weight: 700;\
|
font-weight: 700;\
|
||||||
font-size: 16px;\
|
font-size: 24px;\
|
||||||
color: #a8aaaf;\
|
color: #a8aaaf;\
|
||||||
text-decoration: none;\
|
text-decoration: none;\
|
||||||
')
|
')
|
||||||
|
|||||||
173
src/components/Common/SettingsTabs/index.tsx
Normal file
173
src/components/Common/SettingsTabs/index.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React from 'react';
|
||||||
|
import { hasPermission, Permission } from '../../../../server/lib/permissions';
|
||||||
|
import { useUser } from '../../../hooks/useUser';
|
||||||
|
|
||||||
|
export interface SettingsRoute {
|
||||||
|
text: string;
|
||||||
|
content?: React.ReactNode;
|
||||||
|
route: string;
|
||||||
|
regex: RegExp;
|
||||||
|
requiredPermission?: Permission | Permission[];
|
||||||
|
permissionType?: { type: 'and' | 'or' };
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsLink: React.FC<{
|
||||||
|
tabType: 'default' | 'button';
|
||||||
|
currentPath: string;
|
||||||
|
route: string;
|
||||||
|
regex: RegExp;
|
||||||
|
hidden?: boolean;
|
||||||
|
isMobile?: boolean;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
tabType,
|
||||||
|
currentPath,
|
||||||
|
route,
|
||||||
|
regex,
|
||||||
|
hidden = false,
|
||||||
|
isMobile = false,
|
||||||
|
}) => {
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return <option value={route}>{children}</option>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let linkClasses =
|
||||||
|
'px-1 py-4 ml-8 text-sm font-medium leading-5 transition duration-300 border-b-2 border-transparent whitespace-nowrap first:ml-0';
|
||||||
|
let activeLinkColor = 'text-indigo-500 border-indigo-600';
|
||||||
|
let inactiveLinkColor =
|
||||||
|
'text-gray-500 border-transparent hover:text-gray-300 hover:border-gray-400 focus:text-gray-300 focus:border-gray-400';
|
||||||
|
|
||||||
|
if (tabType === 'button') {
|
||||||
|
linkClasses =
|
||||||
|
'px-3 py-2 ml-8 text-sm font-medium transition duration-300 rounded-md whitespace-nowrap first:ml-0';
|
||||||
|
activeLinkColor = 'bg-indigo-700';
|
||||||
|
inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={route}>
|
||||||
|
<a
|
||||||
|
className={`${linkClasses} ${
|
||||||
|
currentPath.match(regex) ? activeLinkColor : inactiveLinkColor
|
||||||
|
}`}
|
||||||
|
aria-current="page"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsTabs: React.FC<{
|
||||||
|
tabType?: 'default' | 'button';
|
||||||
|
settingsRoutes: SettingsRoute[];
|
||||||
|
}> = ({ tabType = 'default', settingsRoutes }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { user: currentUser } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sm:hidden">
|
||||||
|
<label htmlFor="tabs" className="sr-only">
|
||||||
|
Select a Tab
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
onChange={(e) => {
|
||||||
|
router.push(e.target.value);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
router.push(e.target.value);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
settingsRoutes.find((route) => !!router.pathname.match(route.regex))
|
||||||
|
?.route
|
||||||
|
}
|
||||||
|
aria-label="Selected Tab"
|
||||||
|
>
|
||||||
|
{settingsRoutes
|
||||||
|
.filter(
|
||||||
|
(route) =>
|
||||||
|
!route.hidden &&
|
||||||
|
(route.requiredPermission
|
||||||
|
? hasPermission(
|
||||||
|
route.requiredPermission,
|
||||||
|
currentUser?.permissions ?? 0,
|
||||||
|
route.permissionType
|
||||||
|
)
|
||||||
|
: true)
|
||||||
|
)
|
||||||
|
.map((route, index) => (
|
||||||
|
<SettingsLink
|
||||||
|
tabType={tabType}
|
||||||
|
currentPath={router.pathname}
|
||||||
|
route={route.route}
|
||||||
|
regex={route.regex}
|
||||||
|
hidden={route.hidden ?? false}
|
||||||
|
isMobile
|
||||||
|
key={`mobile-settings-link-${index}`}
|
||||||
|
>
|
||||||
|
{route.text}
|
||||||
|
</SettingsLink>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{tabType === 'button' ? (
|
||||||
|
<div className="hidden overflow-x-scroll overflow-y-hidden sm:block hide-scrollbar">
|
||||||
|
<nav className="flex space-x-4" aria-label="Tabs">
|
||||||
|
{settingsRoutes.map((route, index) => (
|
||||||
|
<SettingsLink
|
||||||
|
tabType={tabType}
|
||||||
|
currentPath={router.pathname}
|
||||||
|
route={route.route}
|
||||||
|
regex={route.regex}
|
||||||
|
hidden={route.hidden ?? false}
|
||||||
|
key={`button-settings-link-${index}`}
|
||||||
|
>
|
||||||
|
{route.content ?? route.text}
|
||||||
|
</SettingsLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<div className="border-b border-gray-600">
|
||||||
|
<nav className="flex -mb-px">
|
||||||
|
{settingsRoutes
|
||||||
|
.filter(
|
||||||
|
(route) =>
|
||||||
|
!route.hidden &&
|
||||||
|
(route.requiredPermission
|
||||||
|
? hasPermission(
|
||||||
|
route.requiredPermission,
|
||||||
|
currentUser?.permissions ?? 0,
|
||||||
|
route.permissionType
|
||||||
|
)
|
||||||
|
: true)
|
||||||
|
)
|
||||||
|
.map((route, index) => (
|
||||||
|
<SettingsLink
|
||||||
|
tabType={tabType}
|
||||||
|
currentPath={router.pathname}
|
||||||
|
route={route.route}
|
||||||
|
regex={route.regex}
|
||||||
|
key={`standard-settings-link-${index}`}
|
||||||
|
>
|
||||||
|
{route.text}
|
||||||
|
</SettingsLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsTabs;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NotificationItem, hasNotificationType } from '..';
|
import { hasNotificationType, NotificationItem } from '..';
|
||||||
|
|
||||||
interface NotificationTypeProps {
|
interface NotificationTypeProps {
|
||||||
option: NotificationItem;
|
option: NotificationItem;
|
||||||
@@ -46,7 +46,7 @@ const NotificationType: React.FC<NotificationTypeProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-sm leading-6">
|
<div className="ml-3 text-sm leading-6">
|
||||||
<label htmlFor={option.id} className="font-medium">
|
<label htmlFor={option.id} className="font-medium text-white">
|
||||||
{option.name}
|
{option.name}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-500">{option.description}</p>
|
<p className="text-gray-500">{option.description}</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||||||
import NotificationType from './NotificationType';
|
import NotificationType from './NotificationType';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
notificationTypes: 'Notification Types',
|
||||||
mediarequested: 'Media Requested',
|
mediarequested: 'Media Requested',
|
||||||
mediarequestedDescription:
|
mediarequestedDescription:
|
||||||
'Sends a notification when media is requested and requires approval.',
|
'Sends a notification when media is requested and requires approval.',
|
||||||
@@ -111,7 +112,14 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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>
|
||||||
|
</span>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="max-w-lg">
|
||||||
{types.map((type) => (
|
{types.map((type) => (
|
||||||
<NotificationType
|
<NotificationType
|
||||||
key={`notification-type-${type.id}`}
|
key={`notification-type-${type.id}`}
|
||||||
@@ -120,7 +128,10 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
|
|||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ const messages = defineMessages({
|
|||||||
webhookUrlPlaceholder: 'Server Settings → Integrations → Webhooks',
|
webhookUrlPlaceholder: 'Server Settings → Integrations → Webhooks',
|
||||||
discordsettingssaved: 'Discord notification settings saved successfully!',
|
discordsettingssaved: 'Discord notification settings saved successfully!',
|
||||||
discordsettingsfailed: 'Discord notification settings failed to save.',
|
discordsettingsfailed: 'Discord notification settings failed to save.',
|
||||||
testsent: 'Test notification sent!',
|
testsent: 'Discord test notification sent!',
|
||||||
notificationtypes: 'Notification Types',
|
|
||||||
validationUrl: 'You must provide a valid URL',
|
validationUrl: 'You must provide a valid URL',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,7 +34,13 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
.nullable()
|
.nullable()
|
||||||
.url(intl.formatMessage(messages.validationUrl)),
|
.url(intl.formatMessage(messages.validationUrl)),
|
||||||
webhookUrl: Yup.string()
|
webhookUrl: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationUrl))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationUrl)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.url(intl.formatMessage(messages.validationUrl)),
|
.url(intl.formatMessage(messages.validationUrl)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +69,7 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
webhookUrl: values.webhookUrl,
|
webhookUrl: values.webhookUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -163,26 +169,10 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import LoadingSpinner from '../../Common/LoadingSpinner';
|
|||||||
import NotificationTypeSelector from '../../NotificationTypeSelector';
|
import NotificationTypeSelector from '../../NotificationTypeSelector';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
validationSmtpHostRequired: 'You must provide a hostname or IP address',
|
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
||||||
validationSmtpPortRequired: 'You must provide a valid port number',
|
validationSmtpPortRequired: 'You must provide a valid port number',
|
||||||
agentenabled: 'Enable Agent',
|
agentenabled: 'Enable Agent',
|
||||||
emailsender: 'Sender Address',
|
emailsender: 'Sender Address',
|
||||||
@@ -24,34 +24,32 @@ const messages = defineMessages({
|
|||||||
authPass: 'SMTP Password',
|
authPass: 'SMTP Password',
|
||||||
emailsettingssaved: 'Email notification settings saved successfully!',
|
emailsettingssaved: 'Email notification settings saved successfully!',
|
||||||
emailsettingsfailed: 'Email notification settings failed to save.',
|
emailsettingsfailed: 'Email notification settings failed to save.',
|
||||||
testsent: 'Test notification sent!',
|
testsent: 'Email test notification sent!',
|
||||||
allowselfsigned: 'Allow Self-Signed Certificates',
|
allowselfsigned: 'Allow Self-Signed Certificates',
|
||||||
ssldisabletip:
|
ssldisabletip:
|
||||||
'SSL should be disabled on standard TLS connections (port 587)',
|
'SSL should be disabled on standard TLS connections (port 587)',
|
||||||
senderName: 'Sender Name',
|
senderName: 'Sender Name',
|
||||||
notificationtypes: 'Notification Types',
|
|
||||||
validationEmail: 'You must provide a valid email address',
|
validationEmail: 'You must provide a valid email address',
|
||||||
emailNotificationTypesAlert: 'Email Notification Recipients',
|
emailNotificationTypesAlert: 'Email Notification Recipients',
|
||||||
emailNotificationTypesAlertDescription:
|
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.',
|
'<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:
|
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.',
|
'<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: '<PgpLink>PGP</PgpLink> Private Key',
|
pgpPrivateKey: 'PGP Private Key',
|
||||||
pgpPrivateKeyTip:
|
pgpPrivateKeyTip:
|
||||||
'Sign encrypted email messages (PGP password is also required)',
|
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||||
pgpPassword: '<PgpLink>PGP</PgpLink> Password',
|
validationPgpPrivateKey:
|
||||||
|
'You must provide a valid PGP private key if a PGP password is entered',
|
||||||
|
pgpPassword: 'PGP Password',
|
||||||
pgpPasswordTip:
|
pgpPasswordTip:
|
||||||
'Sign encrypted email messages (PGP private key is also required)',
|
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
|
||||||
|
validationPgpPassword:
|
||||||
|
'You must provide a PGP password if a PGP private key is entered',
|
||||||
});
|
});
|
||||||
|
|
||||||
export function PgpLink(msg: string): JSX.Element {
|
export function OpenPgpLink(msg: string): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<a
|
<a href="https://www.openpgp.org/" target="_blank" rel="noreferrer">
|
||||||
href="https://www.openpgp.org/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-gray-100 underline transition duration-300 hover:text-white"
|
|
||||||
>
|
|
||||||
{msg}
|
{msg}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -64,21 +62,60 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
'/api/v1/settings/notifications/email'
|
'/api/v1/settings/notifications/email'
|
||||||
);
|
);
|
||||||
|
|
||||||
const NotificationsEmailSchema = Yup.object().shape({
|
const NotificationsEmailSchema = Yup.object().shape(
|
||||||
|
{
|
||||||
emailFrom: Yup.string()
|
emailFrom: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationEmail))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationEmail)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.email(intl.formatMessage(messages.validationEmail)),
|
.email(intl.formatMessage(messages.validationEmail)),
|
||||||
smtpHost: Yup.string()
|
smtpHost: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationSmtpHostRequired))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationSmtpHostRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.matches(
|
.matches(
|
||||||
// eslint-disable-next-line
|
|
||||||
/^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
|
/^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
|
||||||
intl.formatMessage(messages.validationSmtpHostRequired)
|
intl.formatMessage(messages.validationSmtpHostRequired)
|
||||||
),
|
),
|
||||||
smtpPort: Yup.number()
|
smtpPort: Yup.number()
|
||||||
.typeError(intl.formatMessage(messages.validationSmtpPortRequired))
|
.typeError(intl.formatMessage(messages.validationSmtpPortRequired))
|
||||||
.required(intl.formatMessage(messages.validationSmtpPortRequired)),
|
.when('enabled', {
|
||||||
});
|
is: true,
|
||||||
|
then: Yup.number().required(
|
||||||
|
intl.formatMessage(messages.validationSmtpPortRequired)
|
||||||
|
),
|
||||||
|
otherwise: Yup.number().nullable(),
|
||||||
|
}),
|
||||||
|
pgpPrivateKey: Yup.string()
|
||||||
|
.when('pgpPassword', {
|
||||||
|
is: (value: unknown) => !!value,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationPgpPrivateKey)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
|
.matches(
|
||||||
|
/^-----BEGIN PGP PRIVATE KEY BLOCK-----.+-----END PGP PRIVATE KEY BLOCK-----$/,
|
||||||
|
intl.formatMessage(messages.validationPgpPrivateKey)
|
||||||
|
),
|
||||||
|
pgpPassword: Yup.string().when('pgpPrivateKey', {
|
||||||
|
is: (value: unknown) => !!value,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationPgpPassword)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
[['pgpPrivateKey', 'pgpPassword']]
|
||||||
|
);
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -119,6 +156,7 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
pgpPassword: values.pgpPassword,
|
pgpPassword: values.pgpPassword,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -323,15 +361,15 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="pgpPrivateKey" className="text-label">
|
<label htmlFor="pgpPrivateKey" className="text-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
{intl.formatMessage(messages.pgpPrivateKey, {
|
{intl.formatMessage(messages.pgpPrivateKey)}
|
||||||
PgpLink: PgpLink,
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
<Badge badgeType="danger">
|
<Badge badgeType="danger">
|
||||||
{intl.formatMessage(globalMessages.advanced)}
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="label-tip">
|
<span className="label-tip">
|
||||||
{intl.formatMessage(messages.pgpPrivateKeyTip)}
|
{intl.formatMessage(messages.pgpPrivateKeyTip, {
|
||||||
|
OpenPgpLink: OpenPgpLink,
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
@@ -340,23 +378,27 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
id="pgpPrivateKey"
|
id="pgpPrivateKey"
|
||||||
name="pgpPrivateKey"
|
name="pgpPrivateKey"
|
||||||
as="textarea"
|
as="textarea"
|
||||||
rows="3"
|
rows="10"
|
||||||
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.pgpPrivateKey && touched.pgpPrivateKey && (
|
||||||
|
<div className="error">{errors.pgpPrivateKey}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="pgpPassword" className="text-label">
|
<label htmlFor="pgpPassword" className="text-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
{intl.formatMessage(messages.pgpPassword, {
|
{intl.formatMessage(messages.pgpPassword)}
|
||||||
PgpLink: PgpLink,
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
<Badge badgeType="danger">
|
<Badge badgeType="danger">
|
||||||
{intl.formatMessage(globalMessages.advanced)}
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="label-tip">
|
<span className="label-tip">
|
||||||
{intl.formatMessage(messages.pgpPasswordTip)}
|
{intl.formatMessage(messages.pgpPasswordTip, {
|
||||||
|
OpenPgpLink: OpenPgpLink,
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
@@ -368,30 +410,15 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.pgpPassword && touched.pgpPassword && (
|
||||||
|
<div className="error">{errors.pgpPassword}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) =>
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
setFieldValue('types', newTypes)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -18,11 +18,10 @@ const messages = defineMessages({
|
|||||||
pushbulletSettingsSaved:
|
pushbulletSettingsSaved:
|
||||||
'Pushbullet notification settings saved successfully!',
|
'Pushbullet notification settings saved successfully!',
|
||||||
pushbulletSettingsFailed: 'Pushbullet notification settings failed to save.',
|
pushbulletSettingsFailed: 'Pushbullet notification settings failed to save.',
|
||||||
testSent: 'Test notification sent!',
|
testSent: 'Pushbullet test notification sent!',
|
||||||
settingUpPushbullet: 'Setting Up Pushbullet Notifications',
|
settingUpPushbullet: 'Setting Up Pushbullet Notifications',
|
||||||
settingUpPushbulletDescription:
|
settingUpPushbulletDescription:
|
||||||
'To configure Pushbullet notifications, you will need to <CreateAccessTokenLink>create an access token</CreateAccessTokenLink> and enter it below.',
|
'To configure Pushbullet notifications, you will need to <CreateAccessTokenLink>create an access token</CreateAccessTokenLink>.',
|
||||||
notificationTypes: 'Notification Types',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsPushbullet: React.FC = () => {
|
const NotificationsPushbullet: React.FC = () => {
|
||||||
@@ -33,9 +32,13 @@ const NotificationsPushbullet: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const NotificationsPushbulletSchema = Yup.object().shape({
|
const NotificationsPushbulletSchema = Yup.object().shape({
|
||||||
accessToken: Yup.string().required(
|
accessToken: Yup.string().when('enabled', {
|
||||||
intl.formatMessage(messages.validationAccessTokenRequired)
|
is: true,
|
||||||
),
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationAccessTokenRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
@@ -138,28 +141,10 @@ const NotificationsPushbullet: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) =>
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
setFieldValue('types', newTypes)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
agentenabled: 'Enable Agent',
|
agentenabled: 'Enable Agent',
|
||||||
accessToken: 'Application/API Token',
|
accessToken: 'Application/API Token',
|
||||||
userToken: 'User Key',
|
userToken: 'User or Group Key',
|
||||||
validationAccessTokenRequired: 'You must provide a valid application token',
|
validationAccessTokenRequired: 'You must provide a valid application token',
|
||||||
validationUserTokenRequired: 'You must provide a valid user key',
|
validationUserTokenRequired: 'You must provide a valid user key',
|
||||||
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||||
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
||||||
testsent: 'Test notification sent!',
|
testsent: 'Pushover test notification sent!',
|
||||||
settinguppushover: 'Setting Up Pushover Notifications',
|
settinguppushover: 'Setting Up Pushover Notifications',
|
||||||
settinguppushoverDescription:
|
settinguppushoverDescription:
|
||||||
'To configure Pushover notifications, you will need to <RegisterApplicationLink>register an application</RegisterApplicationLink> and enter the API token below. (You can use one of our <IconLink>official icons on GitHub</IconLink>.) You will also need your user key.',
|
'To configure Pushover notifications, you will need to <RegisterApplicationLink>register an application</RegisterApplicationLink> and enter the API token below. (You can use one of the <IconLink>official Overseerr icons on GitHub</IconLink>.)',
|
||||||
notificationtypes: 'Notification Types',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsPushover: React.FC = () => {
|
const NotificationsPushover: React.FC = () => {
|
||||||
@@ -35,13 +34,25 @@ const NotificationsPushover: React.FC = () => {
|
|||||||
|
|
||||||
const NotificationsPushoverSchema = Yup.object().shape({
|
const NotificationsPushoverSchema = Yup.object().shape({
|
||||||
accessToken: Yup.string()
|
accessToken: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationAccessTokenRequired))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationAccessTokenRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.matches(
|
.matches(
|
||||||
/^[a-z\d]{30}$/i,
|
/^[a-z\d]{30}$/i,
|
||||||
intl.formatMessage(messages.validationAccessTokenRequired)
|
intl.formatMessage(messages.validationAccessTokenRequired)
|
||||||
),
|
),
|
||||||
userToken: Yup.string()
|
userToken: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationUserTokenRequired))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationUserTokenRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.matches(
|
.matches(
|
||||||
/^[a-z\d]{30}$/i,
|
/^[a-z\d]{30}$/i,
|
||||||
intl.formatMessage(messages.validationUserTokenRequired)
|
intl.formatMessage(messages.validationUserTokenRequired)
|
||||||
@@ -182,28 +193,10 @@ const NotificationsPushover: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) =>
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
setFieldValue('types', newTypes)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ const messages = defineMessages({
|
|||||||
webhookUrl: 'Webhook URL',
|
webhookUrl: 'Webhook URL',
|
||||||
slacksettingssaved: 'Slack notification settings saved successfully!',
|
slacksettingssaved: 'Slack notification settings saved successfully!',
|
||||||
slacksettingsfailed: 'Slack notification settings failed to save.',
|
slacksettingsfailed: 'Slack notification settings failed to save.',
|
||||||
testsent: 'Test notification sent!',
|
testsent: 'Slack test notification sent!',
|
||||||
settingupslack: 'Setting Up Slack Notifications',
|
settingupslack: 'Setting Up Slack Notifications',
|
||||||
settingupslackDescription:
|
settingupslackDescription:
|
||||||
'To configure Slack notifications, you will need to create an <WebhookLink>Incoming Webhook</WebhookLink> integration and enter the webhook URL below.',
|
'To configure Slack notifications, you will need to create an <WebhookLink>Incoming Webhook</WebhookLink> integration and enter the webhook URL below.',
|
||||||
notificationtypes: 'Notification Types',
|
|
||||||
validationWebhookUrl: 'You must provide a valid URL',
|
validationWebhookUrl: 'You must provide a valid URL',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,7 +32,13 @@ const NotificationsSlack: React.FC = () => {
|
|||||||
|
|
||||||
const NotificationsSlackSchema = Yup.object().shape({
|
const NotificationsSlackSchema = Yup.object().shape({
|
||||||
webhookUrl: Yup.string()
|
webhookUrl: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationWebhookUrl))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationWebhookUrl)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.url(intl.formatMessage(messages.validationWebhookUrl)),
|
.url(intl.formatMessage(messages.validationWebhookUrl)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,28 +141,10 @@ const NotificationsSlack: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) =>
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
setFieldValue('types', newTypes)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ import NotificationTypeSelector from '../../NotificationTypeSelector';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
agentenabled: 'Enable Agent',
|
agentenabled: 'Enable Agent',
|
||||||
botUsername: 'Bot Username',
|
botUsername: 'Bot Username',
|
||||||
|
botUsernameTip:
|
||||||
|
'Allow users to start a chat with the bot and configure their own personal notifications',
|
||||||
botAPI: 'Bot Authentication Token',
|
botAPI: 'Bot Authentication Token',
|
||||||
chatId: 'Chat ID',
|
chatId: 'Chat ID',
|
||||||
validationBotAPIRequired: 'You must provide a bot authentication token',
|
validationBotAPIRequired: 'You must provide a bot authentication token',
|
||||||
validationChatIdRequired: 'You must provide a valid chat ID',
|
validationChatIdRequired: 'You must provide a valid chat ID',
|
||||||
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
||||||
telegramsettingsfailed: 'Telegram notification settings failed to save.',
|
telegramsettingsfailed: 'Telegram notification settings failed to save.',
|
||||||
testsent: 'Test notification sent!',
|
testsent: 'Telegram test notification sent!',
|
||||||
settinguptelegram: 'Setting Up Telegram Notifications',
|
settinguptelegram: 'Setting Up Telegram Notifications',
|
||||||
settinguptelegramDescription:
|
settinguptelegramDescription:
|
||||||
'To configure Telegram notifications, you will need to <CreateBotLink>create a bot</CreateBotLink> and get the bot API key. Additionally, you will need the chat ID for the chat to which you would like to send notifications. You can find this by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat and issuing the <code>/my_id</code> command.',
|
'To configure Telegram notifications, you will need to <CreateBotLink>create a bot</CreateBotLink> and get the bot API key. Additionally, you will need the chat ID for the chat to which you would like to send notifications. You can find this by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat and issuing the <code>/my_id</code> command.',
|
||||||
notificationtypes: 'Notification Types',
|
|
||||||
sendSilently: 'Send Silently',
|
sendSilently: 'Send Silently',
|
||||||
sendSilentlyTip: 'Send notifications with no sound',
|
sendSilentlyTip: 'Send notifications with no sound',
|
||||||
});
|
});
|
||||||
@@ -37,13 +38,23 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const NotificationsTelegramSchema = Yup.object().shape({
|
const NotificationsTelegramSchema = Yup.object().shape({
|
||||||
botAPI: Yup.string().required(
|
botAPI: Yup.string().when('enabled', {
|
||||||
intl.formatMessage(messages.validationBotAPIRequired)
|
is: true,
|
||||||
),
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationBotAPIRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
}),
|
||||||
chatId: Yup.string()
|
chatId: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationChatIdRequired))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationChatIdRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.matches(
|
.matches(
|
||||||
/^[-]?\d+$/,
|
/^-?\d+$/,
|
||||||
intl.formatMessage(messages.validationChatIdRequired)
|
intl.formatMessage(messages.validationChatIdRequired)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -75,6 +86,7 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
botUsername: values.botUsername,
|
botUsername: values.botUsername,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -156,6 +168,9 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="botUsername" className="text-label">
|
<label htmlFor="botUsername" className="text-label">
|
||||||
{intl.formatMessage(messages.botUsername)}
|
{intl.formatMessage(messages.botUsername)}
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.botUsernameTip)}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
@@ -224,28 +239,10 @@ const NotificationsTelegram: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</span>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) =>
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
setFieldValue('types', newTypes)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ const messages = defineMessages({
|
|||||||
validationJsonPayloadRequired: 'You must provide a valid JSON payload',
|
validationJsonPayloadRequired: 'You must provide a valid JSON payload',
|
||||||
webhooksettingssaved: 'Webhook notification settings saved successfully!',
|
webhooksettingssaved: 'Webhook notification settings saved successfully!',
|
||||||
webhooksettingsfailed: 'Webhook notification settings failed to save.',
|
webhooksettingsfailed: 'Webhook notification settings failed to save.',
|
||||||
testsent: 'Test notification sent!',
|
testsent: 'Webhook test notification sent!',
|
||||||
notificationtypes: 'Notification Types',
|
|
||||||
resetPayload: 'Reset to Default',
|
resetPayload: 'Reset to Default',
|
||||||
resetPayloadSuccess: 'JSON payload reset successfully!',
|
resetPayloadSuccess: 'JSON payload reset successfully!',
|
||||||
customJson: 'JSON Payload',
|
customJson: 'JSON Payload',
|
||||||
@@ -63,14 +62,26 @@ const NotificationsWebhook: React.FC = () => {
|
|||||||
|
|
||||||
const NotificationsWebhookSchema = Yup.object().shape({
|
const NotificationsWebhookSchema = Yup.object().shape({
|
||||||
webhookUrl: Yup.string()
|
webhookUrl: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationWebhookUrl))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationWebhookUrl)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.matches(
|
.matches(
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
/^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,
|
/^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,
|
||||||
intl.formatMessage(messages.validationWebhookUrl)
|
intl.formatMessage(messages.validationWebhookUrl)
|
||||||
),
|
),
|
||||||
jsonPayload: Yup.string()
|
jsonPayload: Yup.string()
|
||||||
.required(intl.formatMessage(messages.validationJsonPayloadRequired))
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationJsonPayloadRequired)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
.test(
|
.test(
|
||||||
'validate-json',
|
'validate-json',
|
||||||
intl.formatMessage(messages.validationJsonPayloadRequired),
|
intl.formatMessage(messages.validationJsonPayloadRequired),
|
||||||
@@ -258,32 +269,10 @@ const NotificationsWebhook: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8">
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
aria-labelledby="group-label"
|
|
||||||
className="form-group"
|
|
||||||
>
|
|
||||||
<div className="sm:grid sm:grid-cols-4 sm:gap-4">
|
|
||||||
<div>
|
|
||||||
<div id="group-label" className="group-label">
|
|
||||||
{intl.formatMessage(messages.notificationtypes)}
|
|
||||||
<span className="label-required">*</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<NotificationTypeSelector
|
<NotificationTypeSelector
|
||||||
currentTypes={values.types}
|
currentTypes={values.types}
|
||||||
onUpdate={(newTypes) =>
|
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
|
||||||
setFieldValue('types', newTypes)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import PageTitle from '../Common/PageTitle';
|
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
|
import PageTitle from '../Common/PageTitle';
|
||||||
|
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
menuGeneralSettings: 'General',
|
menuGeneralSettings: 'General',
|
||||||
@@ -16,14 +15,7 @@ const messages = defineMessages({
|
|||||||
menuAbout: 'About',
|
menuAbout: 'About',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SettingsRoute {
|
|
||||||
text: string;
|
|
||||||
route: string;
|
|
||||||
regex: RegExp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsLayout: React.FC = ({ children }) => {
|
const SettingsLayout: React.FC = ({ children }) => {
|
||||||
const router = useRouter();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const settingsRoutes: SettingsRoute[] = [
|
const settingsRoutes: SettingsRoute[] = [
|
||||||
@@ -69,78 +61,11 @@ const SettingsLayout: React.FC = ({ children }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const activeLinkColor =
|
|
||||||
'border-indigo-600 text-indigo-500 focus:outline-none focus:text-indigo-500 focus:border-indigo-500';
|
|
||||||
|
|
||||||
const inactiveLinkColor =
|
|
||||||
'border-transparent text-gray-500 hover:text-gray-400 hover:border-gray-300 focus:outline-none focus:text-gray-4700 focus:border-gray-300';
|
|
||||||
|
|
||||||
const SettingsLink: React.FC<{
|
|
||||||
route: string;
|
|
||||||
regex: RegExp;
|
|
||||||
isMobile?: boolean;
|
|
||||||
}> = ({ children, route, regex, isMobile = false }) => {
|
|
||||||
if (isMobile) {
|
|
||||||
return <option value={route}>{children}</option>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={route}>
|
|
||||||
<a
|
|
||||||
className={`whitespace-nowrap ml-8 first:ml-0 py-4 px-1 border-b-2 border-transparent font-medium text-sm leading-5 ${
|
|
||||||
router.pathname.match(regex) ? activeLinkColor : inactiveLinkColor
|
|
||||||
}`}
|
|
||||||
aria-current="page"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={intl.formatMessage(globalMessages.settings)} />
|
<PageTitle title={intl.formatMessage(globalMessages.settings)} />
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="sm:hidden">
|
<SettingsTabs settingsRoutes={settingsRoutes} />
|
||||||
<select
|
|
||||||
onChange={(e) => {
|
|
||||||
router.push(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
router.push(e.target.value);
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
settingsRoutes.find(
|
|
||||||
(route) => !!router.pathname.match(route.regex)
|
|
||||||
)?.route
|
|
||||||
}
|
|
||||||
aria-label="Selected tab"
|
|
||||||
>
|
|
||||||
{settingsRoutes.map((route, index) => (
|
|
||||||
<SettingsLink
|
|
||||||
route={route.route}
|
|
||||||
regex={route.regex}
|
|
||||||
isMobile
|
|
||||||
key={`mobile-settings-link-${index}`}
|
|
||||||
>
|
|
||||||
{route.text}
|
|
||||||
</SettingsLink>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="hidden overflow-x-scroll overflow-y-hidden border-b border-gray-600 sm:block hide-scrollbar">
|
|
||||||
<nav className="flex -mb-px">
|
|
||||||
{settingsRoutes.map((route, index) => (
|
|
||||||
<SettingsLink
|
|
||||||
route={route.route}
|
|
||||||
regex={route.regex}
|
|
||||||
key={`standard-settings-link-${index}`}
|
|
||||||
>
|
|
||||||
{route.text}
|
|
||||||
</SettingsLink>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 text-white">{children}</div>
|
<div className="mt-10 text-white">{children}</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { Field, Form, Formik } from 'formik';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import Bolt from '../../assets/bolt.svg';
|
import Bolt from '../../assets/bolt.svg';
|
||||||
import DiscordLogo from '../../assets/extlogos/discord.svg';
|
import DiscordLogo from '../../assets/extlogos/discord.svg';
|
||||||
import PushbulletLogo from '../../assets/extlogos/pushbullet.svg';
|
import PushbulletLogo from '../../assets/extlogos/pushbullet.svg';
|
||||||
@@ -13,38 +7,22 @@ import PushoverLogo from '../../assets/extlogos/pushover.svg';
|
|||||||
import SlackLogo from '../../assets/extlogos/slack.svg';
|
import SlackLogo from '../../assets/extlogos/slack.svg';
|
||||||
import TelegramLogo from '../../assets/extlogos/telegram.svg';
|
import TelegramLogo from '../../assets/extlogos/telegram.svg';
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
import Error from '../../pages/_error';
|
|
||||||
import Button from '../Common/Button';
|
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
|
||||||
import PageTitle from '../Common/PageTitle';
|
import PageTitle from '../Common/PageTitle';
|
||||||
|
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notificationsettings: 'Notification Settings',
|
notificationsettings: 'Notification Settings',
|
||||||
notificationsettingsDescription:
|
|
||||||
'Configure global notification settings. The options below will apply to all notification agents.',
|
|
||||||
notificationAgentsSettings: 'Notification Agents',
|
|
||||||
notificationAgentSettingsDescription:
|
notificationAgentSettingsDescription:
|
||||||
'Choose the types of notifications to send, and which notification agents to use.',
|
'Configure and enable notification agents.',
|
||||||
notificationsettingssaved: 'Notification settings saved successfully!',
|
notificationsettingssaved: 'Notification settings saved successfully!',
|
||||||
notificationsettingsfailed: 'Notification settings failed to save.',
|
notificationsettingsfailed: 'Notification settings failed to save.',
|
||||||
enablenotifications: 'Enable Notifications',
|
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
webhook: 'Webhook',
|
webhook: 'Webhook',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SettingsRoute {
|
|
||||||
text: string;
|
|
||||||
content: React.ReactNode;
|
|
||||||
route: string;
|
|
||||||
regex: RegExp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsNotifications: React.FC = ({ children }) => {
|
const SettingsNotifications: React.FC = ({ children }) => {
|
||||||
const router = useRouter();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { addToast } = useToasts();
|
|
||||||
const { data, error, revalidate } = useSWR('/api/v1/settings/notifications');
|
|
||||||
|
|
||||||
const settingsRoutes: SettingsRoute[] = [
|
const settingsRoutes: SettingsRoute[] = [
|
||||||
{
|
{
|
||||||
@@ -139,40 +117,6 @@ const SettingsNotifications: React.FC = ({ children }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const activeLinkColor = 'bg-indigo-700';
|
|
||||||
const inactiveLinkColor = 'bg-gray-800';
|
|
||||||
|
|
||||||
const SettingsLink: React.FC<{
|
|
||||||
route: string;
|
|
||||||
regex: RegExp;
|
|
||||||
isMobile?: boolean;
|
|
||||||
}> = ({ children, route, regex, isMobile = false }) => {
|
|
||||||
if (isMobile) {
|
|
||||||
return <option value={route}>{children}</option>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={route}>
|
|
||||||
<a
|
|
||||||
className={`whitespace-nowrap ml-8 first:ml-0 px-3 py-2 font-medium text-sm rounded-md ${
|
|
||||||
router.pathname.match(regex) ? activeLinkColor : inactiveLinkColor
|
|
||||||
}`}
|
|
||||||
aria-current="page"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!data && !error) {
|
|
||||||
return <LoadingSpinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <Error statusCode={500} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
@@ -185,131 +129,11 @@ const SettingsNotifications: React.FC = ({ children }) => {
|
|||||||
<h3 className="heading">
|
<h3 className="heading">
|
||||||
{intl.formatMessage(messages.notificationsettings)}
|
{intl.formatMessage(messages.notificationsettings)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="description">
|
|
||||||
{intl.formatMessage(messages.notificationsettingsDescription)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="section">
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
enabled: data.enabled,
|
|
||||||
}}
|
|
||||||
enableReinitialize
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
try {
|
|
||||||
await axios.post('/api/v1/settings/notifications', {
|
|
||||||
enabled: values.enabled,
|
|
||||||
});
|
|
||||||
addToast(intl.formatMessage(messages.notificationsettingssaved), {
|
|
||||||
appearance: 'success',
|
|
||||||
autoDismiss: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
addToast(
|
|
||||||
intl.formatMessage(messages.notificationsettingsfailed),
|
|
||||||
{
|
|
||||||
appearance: 'error',
|
|
||||||
autoDismiss: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
revalidate();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ isSubmitting, values, setFieldValue }) => {
|
|
||||||
return (
|
|
||||||
<Form className="section">
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="name" className="checkbox-label">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage(messages.enablenotifications)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
name="enabled"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('enabled', !values.enabled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting
|
|
||||||
? intl.formatMessage(globalMessages.saving)
|
|
||||||
: intl.formatMessage(globalMessages.save)}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
<div className="mt-10 mb-6">
|
|
||||||
<h3 className="heading">
|
|
||||||
{intl.formatMessage(messages.notificationAgentsSettings)}
|
|
||||||
</h3>
|
|
||||||
<p className="description">
|
<p className="description">
|
||||||
{intl.formatMessage(messages.notificationAgentSettingsDescription)}
|
{intl.formatMessage(messages.notificationAgentSettingsDescription)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<SettingsTabs tabType="button" settingsRoutes={settingsRoutes} />
|
||||||
<div className="sm:hidden">
|
|
||||||
<label htmlFor="tabs" className="sr-only">
|
|
||||||
Select a tab
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
onChange={(e) => {
|
|
||||||
router.push(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
router.push(e.target.value);
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
settingsRoutes.find(
|
|
||||||
(route) => !!router.pathname.match(route.regex)
|
|
||||||
)?.route
|
|
||||||
}
|
|
||||||
aria-label="Selected tab"
|
|
||||||
>
|
|
||||||
{settingsRoutes.map((route, index) => (
|
|
||||||
<SettingsLink
|
|
||||||
route={route.route}
|
|
||||||
regex={route.regex}
|
|
||||||
isMobile
|
|
||||||
key={`mobile-settings-link-${index}`}
|
|
||||||
>
|
|
||||||
{route.text}
|
|
||||||
</SettingsLink>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="hidden overflow-x-scroll overflow-y-hidden sm:block hide-scrollbar">
|
|
||||||
<nav className="flex space-x-4" aria-label="Tabs">
|
|
||||||
{settingsRoutes.map((route, index) => (
|
|
||||||
<SettingsLink
|
|
||||||
route={route.route}
|
|
||||||
regex={route.regex}
|
|
||||||
key={`standard-settings-link-${index}`}
|
|
||||||
>
|
|
||||||
{route.content}
|
|
||||||
</SettingsLink>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="section">{children}</div>
|
<div className="section">{children}</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
|
import {
|
||||||
|
hasNotificationAgentEnabled,
|
||||||
|
NotificationAgentType,
|
||||||
|
} from '../../../../../server/lib/notifications/agenttypes';
|
||||||
|
import { useUser } from '../../../../hooks/useUser';
|
||||||
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
|
import Button from '../../../Common/Button';
|
||||||
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
|
|
||||||
|
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',
|
||||||
|
validationDiscordId: 'You must provide a valid user ID',
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserNotificationsDiscord: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const router = useRouter();
|
||||||
|
const [notificationAgents, setNotificationAgents] = useState(0);
|
||||||
|
const { user } = useUser({ id: Number(router.query.userId) });
|
||||||
|
const { data, error, revalidate } = useSWR<UserSettingsNotificationsResponse>(
|
||||||
|
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNotificationAgents(
|
||||||
|
data?.notificationAgents ?? NotificationAgentType.EMAIL
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const UserNotificationsDiscordSchema = Yup.object().shape({
|
||||||
|
discordId: Yup.string()
|
||||||
|
.when('enableDiscord', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationDiscordId)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
|
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
enableDiscord: hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.DISCORD,
|
||||||
|
data?.notificationAgents ?? NotificationAgentType.EMAIL
|
||||||
|
),
|
||||||
|
discordId: data?.discordId,
|
||||||
|
}}
|
||||||
|
validationSchema={UserNotificationsDiscordSchema}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||||
|
notificationAgents,
|
||||||
|
pgpKey: data?.pgpKey,
|
||||||
|
discordId: values.discordId,
|
||||||
|
telegramChatId: data?.telegramChatId,
|
||||||
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
|
});
|
||||||
|
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.discordsettingsfailed), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => {
|
||||||
|
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"
|
||||||
|
checked={hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.DISCORD,
|
||||||
|
notificationAgents
|
||||||
|
)}
|
||||||
|
onChange={() => {
|
||||||
|
setNotificationAgents(
|
||||||
|
hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.DISCORD,
|
||||||
|
notificationAgents
|
||||||
|
)
|
||||||
|
? notificationAgents - NotificationAgentType.DISCORD
|
||||||
|
: notificationAgents + NotificationAgentType.DISCORD
|
||||||
|
);
|
||||||
|
setFieldValue('enableDiscord', !values.enableDiscord);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="discordId" className="text-label">
|
||||||
|
<span>{intl.formatMessage(messages.discordId)}</span>
|
||||||
|
<span className="label-required">*</span>
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.discordIdTip, {
|
||||||
|
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field id="discordId" name="discordId" type="text" />
|
||||||
|
</div>
|
||||||
|
{errors.discordId && touched.discordId && (
|
||||||
|
<div className="error">{errors.discordId}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserNotificationsDiscord;
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
|
import {
|
||||||
|
hasNotificationAgentEnabled,
|
||||||
|
NotificationAgentType,
|
||||||
|
} from '../../../../../server/lib/notifications/agenttypes';
|
||||||
|
import { useUser } from '../../../../hooks/useUser';
|
||||||
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
|
import Badge from '../../../Common/Badge';
|
||||||
|
import Button from '../../../Common/Button';
|
||||||
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
|
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>',
|
||||||
|
validationPgpPublicKey: 'You must provide a valid PGP public key',
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserEmailSettings: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const router = useRouter();
|
||||||
|
const [notificationAgents, setNotificationAgents] = useState(0);
|
||||||
|
const { user } = useUser({ id: Number(router.query.userId) });
|
||||||
|
const { data, error, revalidate } = useSWR<UserSettingsNotificationsResponse>(
|
||||||
|
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNotificationAgents(
|
||||||
|
data?.notificationAgents ?? NotificationAgentType.EMAIL
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const UserNotificationsEmailSchema = Yup.object().shape({
|
||||||
|
pgpKey: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.matches(
|
||||||
|
/^-----BEGIN PGP PUBLIC KEY BLOCK-----.+-----END PGP PUBLIC KEY BLOCK-----$/,
|
||||||
|
intl.formatMessage(messages.validationPgpPublicKey)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
enableEmail: hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.EMAIL,
|
||||||
|
data?.notificationAgents ?? NotificationAgentType.EMAIL
|
||||||
|
),
|
||||||
|
pgpKey: data?.pgpKey,
|
||||||
|
}}
|
||||||
|
validationSchema={UserNotificationsEmailSchema}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||||
|
notificationAgents,
|
||||||
|
pgpKey: values.pgpKey,
|
||||||
|
discordId: data?.discordId,
|
||||||
|
telegramChatId: data?.telegramChatId,
|
||||||
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
|
});
|
||||||
|
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.emailsettingsfailed), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => {
|
||||||
|
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"
|
||||||
|
checked={hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.EMAIL,
|
||||||
|
notificationAgents
|
||||||
|
)}
|
||||||
|
onChange={() => {
|
||||||
|
setNotificationAgents(
|
||||||
|
hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.EMAIL,
|
||||||
|
notificationAgents
|
||||||
|
)
|
||||||
|
? notificationAgents - NotificationAgentType.EMAIL
|
||||||
|
: notificationAgents + NotificationAgentType.EMAIL
|
||||||
|
);
|
||||||
|
setFieldValue('enableEmail', !values.enableEmail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="pgpKey" className="text-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.pgpPublicKey)}
|
||||||
|
</span>
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
|
</Badge>
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.pgpPublicKeyTip, {
|
||||||
|
OpenPgpLink: OpenPgpLink,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
as="textarea"
|
||||||
|
id="pgpKey"
|
||||||
|
name="pgpKey"
|
||||||
|
rows="10"
|
||||||
|
className="font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.pgpKey && touched.pgpKey && (
|
||||||
|
<div className="error">{errors.pgpKey}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserEmailSettings;
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
|
import {
|
||||||
|
hasNotificationAgentEnabled,
|
||||||
|
NotificationAgentType,
|
||||||
|
} from '../../../../../server/lib/notifications/agenttypes';
|
||||||
|
import { useUser } from '../../../../hooks/useUser';
|
||||||
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
|
import Button from '../../../Common/Button';
|
||||||
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
|
|
||||||
|
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',
|
||||||
|
sendSilently: 'Send Silently',
|
||||||
|
sendSilentlyDescription: 'Send notifications with no sound',
|
||||||
|
validationTelegramChatId: 'You must provide a valid chat ID',
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserTelegramSettings: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const router = useRouter();
|
||||||
|
const [notificationAgents, setNotificationAgents] = useState(0);
|
||||||
|
const { user } = useUser({ id: Number(router.query.userId) });
|
||||||
|
const { data, error, revalidate } = useSWR<UserSettingsNotificationsResponse>(
|
||||||
|
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNotificationAgents(
|
||||||
|
data?.notificationAgents ?? NotificationAgentType.EMAIL
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const UserNotificationsTelegramSchema = Yup.object().shape({
|
||||||
|
telegramChatId: Yup.string()
|
||||||
|
.when('enableTelegram', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.required(intl.formatMessage(messages.validationTelegramChatId)),
|
||||||
|
otherwise: Yup.string().nullable(),
|
||||||
|
})
|
||||||
|
.matches(
|
||||||
|
/^-?\d+$/,
|
||||||
|
intl.formatMessage(messages.validationTelegramChatId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
enableTelegram: hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.TELEGRAM,
|
||||||
|
data?.notificationAgents ?? NotificationAgentType.EMAIL
|
||||||
|
),
|
||||||
|
telegramChatId: data?.telegramChatId,
|
||||||
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
|
}}
|
||||||
|
validationSchema={UserNotificationsTelegramSchema}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||||
|
notificationAgents,
|
||||||
|
pgpKey: data?.pgpKey,
|
||||||
|
discordId: data?.discordId,
|
||||||
|
telegramChatId: values.telegramChatId,
|
||||||
|
telegramSendSilently: values.telegramSendSilently,
|
||||||
|
});
|
||||||
|
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.telegramsettingsfailed), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => {
|
||||||
|
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"
|
||||||
|
checked={hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.TELEGRAM,
|
||||||
|
notificationAgents
|
||||||
|
)}
|
||||||
|
onChange={() => {
|
||||||
|
setNotificationAgents(
|
||||||
|
hasNotificationAgentEnabled(
|
||||||
|
NotificationAgentType.TELEGRAM,
|
||||||
|
notificationAgents
|
||||||
|
)
|
||||||
|
? notificationAgents - NotificationAgentType.TELEGRAM
|
||||||
|
: notificationAgents + NotificationAgentType.TELEGRAM
|
||||||
|
);
|
||||||
|
setFieldValue('enableTelegram', !values.enableTelegram);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="telegramChatId" className="text-label">
|
||||||
|
{intl.formatMessage(messages.telegramChatId)}
|
||||||
|
<span className="label-required">*</span>
|
||||||
|
{data?.telegramBotUsername && (
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.telegramChatIdTipLong, {
|
||||||
|
TelegramBotLink: function TelegramBotLink(msg) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`https://telegram.me/${data.telegramBotUsername}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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">
|
||||||
|
<Field
|
||||||
|
id="telegramChatId"
|
||||||
|
name="telegramChatId"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.telegramChatId && touched.telegramChatId && (
|
||||||
|
<div className="error">{errors.telegramChatId}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="telegramSendSilently" className="checkbox-label">
|
||||||
|
{intl.formatMessage(messages.sendSilently)}
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.sendSilentlyDescription)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="telegramSendSilently"
|
||||||
|
name="telegramSendSilently"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserTelegramSettings;
|
||||||
@@ -1,61 +1,88 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { Field, Form, Formik } from 'formik';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import * as Yup from 'yup';
|
|
||||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
|
import DiscordLogo from '../../../../assets/extlogos/discord.svg';
|
||||||
|
import TelegramLogo from '../../../../assets/extlogos/telegram.svg';
|
||||||
import { useUser } from '../../../../hooks/useUser';
|
import { useUser } from '../../../../hooks/useUser';
|
||||||
import globalMessages from '../../../../i18n/globalMessages';
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
import Error from '../../../../pages/_error';
|
import Error from '../../../../pages/_error';
|
||||||
import Badge from '../../../Common/Badge';
|
|
||||||
import Button from '../../../Common/Button';
|
|
||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
import PageTitle from '../../../Common/PageTitle';
|
import PageTitle from '../../../Common/PageTitle';
|
||||||
import { PgpLink } from '../../../Settings/Notifications/NotificationsEmail';
|
import SettingsTabs, { SettingsRoute } from '../../../Common/SettingsTabs';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notificationsettings: 'Notification Settings',
|
notificationsettings: 'Notification Settings',
|
||||||
enableNotifications: 'Enable Notifications',
|
email: 'Email',
|
||||||
discordId: 'Discord ID',
|
|
||||||
discordIdTip:
|
|
||||||
'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your Discord user account',
|
|
||||||
validationDiscordId: 'You must provide a valid Discord user ID',
|
|
||||||
telegramChatId: 'Telegram Chat ID',
|
|
||||||
telegramChatIdTip: 'Add <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat',
|
|
||||||
telegramChatIdTipLong:
|
|
||||||
'<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
|
||||||
sendSilently: 'Send Telegram Messages Silently',
|
|
||||||
sendSilentlyDescription: 'Send notifications with no sound',
|
|
||||||
validationTelegramChatId: 'You must provide a valid Telegram chat ID',
|
|
||||||
toastSettingsSuccess: 'Notification settings saved successfully!',
|
toastSettingsSuccess: 'Notification settings saved successfully!',
|
||||||
toastSettingsFailure: 'Something went wrong while saving settings.',
|
toastSettingsFailure: 'Something went wrong while saving settings.',
|
||||||
pgpKey: '<PgpLink>PGP</PgpLink> Public Key',
|
|
||||||
pgpKeyTip: 'Encrypt email messages',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserNotificationSettings: React.FC = () => {
|
const UserNotificationSettings: React.FC = ({ children }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { addToast } = useToasts();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, mutate } = useUser({ id: Number(router.query.userId) });
|
const { user } = useUser({ id: Number(router.query.userId) });
|
||||||
const { data, error, revalidate } = useSWR<UserSettingsNotificationsResponse>(
|
const { data, error } = useSWR<UserSettingsNotificationsResponse>(
|
||||||
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const UserNotificationSettingsSchema = Yup.object().shape({
|
const settingsRoutes: SettingsRoute[] = [
|
||||||
discordId: Yup.string()
|
{
|
||||||
.nullable()
|
text: intl.formatMessage(messages.email),
|
||||||
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
|
content: (
|
||||||
telegramChatId: Yup.string()
|
<span className="flex items-center">
|
||||||
.nullable()
|
<svg
|
||||||
.matches(
|
className="h-4 mr-2"
|
||||||
/^[-]?\d+$/,
|
fill="none"
|
||||||
intl.formatMessage(messages.validationTelegramChatId)
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{intl.formatMessage(messages.email)}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
|
route: '/settings/notifications/email',
|
||||||
|
regex: /\/settings\/notifications\/email/,
|
||||||
|
hidden: !data?.emailEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Discord',
|
||||||
|
content: (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<DiscordLogo className="h-4 mr-2" />
|
||||||
|
Discord
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
route: '/settings/notifications/discord',
|
||||||
|
regex: /\/settings\/notifications\/discord/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Telegram',
|
||||||
|
content: (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<TelegramLogo className="h-4 mr-2" />
|
||||||
|
Telegram
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
route: '/settings/notifications/telegram',
|
||||||
|
regex: /\/settings\/notifications\/telegram/,
|
||||||
|
hidden: !data?.telegramEnabled || !data?.telegramBotUsername,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
settingsRoutes.forEach((settingsRoute) => {
|
||||||
|
settingsRoute.route = router.asPath.includes('/profile')
|
||||||
|
? `/profile${settingsRoute.route}`
|
||||||
|
: `/users/${user?.id}${settingsRoute.route}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
@@ -80,215 +107,8 @@ const UserNotificationSettings: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.notificationsettings)}
|
{intl.formatMessage(messages.notificationsettings)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Formik
|
<SettingsTabs tabType="button" settingsRoutes={settingsRoutes} />
|
||||||
initialValues={{
|
<div className="section">{children}</div>
|
||||||
enableNotifications: data?.enableNotifications,
|
|
||||||
discordId: data?.discordId,
|
|
||||||
telegramChatId: data?.telegramChatId,
|
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
|
||||||
pgpKey: data?.pgpKey,
|
|
||||||
}}
|
|
||||||
validationSchema={UserNotificationSettingsSchema}
|
|
||||||
enableReinitialize
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
`/api/v1/user/${user?.id}/settings/notifications`,
|
|
||||||
{
|
|
||||||
enableNotifications: values.enableNotifications,
|
|
||||||
discordId: values.discordId,
|
|
||||||
telegramChatId: values.telegramChatId,
|
|
||||||
telegramSendSilently: values.telegramSendSilently,
|
|
||||||
pgpKey: values.pgpKey,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'success',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
addToast(intl.formatMessage(messages.toastSettingsFailure), {
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
revalidate();
|
|
||||||
mutate();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ errors, touched, isSubmitting }) => {
|
|
||||||
return (
|
|
||||||
<Form className="section">
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="enableNotifications" className="checkbox-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.enableNotifications)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="enableNotifications"
|
|
||||||
name="enableNotifications"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="pgpKey" className="text-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.pgpKey, {
|
|
||||||
PgpLink: PgpLink,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<Badge badgeType="danger">
|
|
||||||
{intl.formatMessage(globalMessages.advanced)}
|
|
||||||
</Badge>
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.pgpKeyTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field id="pgpKey" name="pgpKey" as="textarea" rows="3" />
|
|
||||||
</div>
|
|
||||||
{errors.pgpKey && touched.pgpKey && (
|
|
||||||
<div className="error">{errors.pgpKey}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="discordId" className="text-label">
|
|
||||||
<span>{intl.formatMessage(messages.discordId)}</span>
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.discordIdTip, {
|
|
||||||
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-gray-100 underline transition duration-300 hover:text-white"
|
|
||||||
>
|
|
||||||
{msg}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field id="discordId" name="discordId" type="text" />
|
|
||||||
</div>
|
|
||||||
{errors.discordId && touched.discordId && (
|
|
||||||
<div className="error">{errors.discordId}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="telegramChatId" className="text-label">
|
|
||||||
<span>{intl.formatMessage(messages.telegramChatId)}</span>
|
|
||||||
<span className="label-tip">
|
|
||||||
{data?.telegramBotUsername
|
|
||||||
? intl.formatMessage(messages.telegramChatIdTipLong, {
|
|
||||||
TelegramBotLink: function TelegramBotLink(msg) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={`https://telegram.me/${data.telegramBotUsername}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-gray-100 underline transition duration-300 hover:text-white"
|
|
||||||
>
|
|
||||||
{msg}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
GetIdBotLink: function GetIdBotLink(msg) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="https://telegram.me/get_id_bot"
|
|
||||||
className="text-gray-100 underline transition duration-300 hover:text-white"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{msg}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
code: function code(msg) {
|
|
||||||
return <code>{msg}</code>;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: intl.formatMessage(messages.telegramChatIdTip, {
|
|
||||||
GetIdBotLink: function GetIdBotLink(msg) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="https://telegram.me/get_id_bot"
|
|
||||||
className="text-gray-100 underline transition duration-300 hover:text-white"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{msg}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="telegramChatId"
|
|
||||||
name="telegramChatId"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.telegramChatId && touched.telegramChatId && (
|
|
||||||
<div className="error">{errors.telegramChatId}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="telegramSendSilently"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.sendSilently)}
|
|
||||||
</span>
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.sendSilentlyDescription)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="telegramSendSilently"
|
|
||||||
name="telegramSendSilently"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting
|
|
||||||
? intl.formatMessage(globalMessages.saving)
|
|
||||||
: intl.formatMessage(globalMessages.save)}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Formik>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
import { hasPermission, Permission } from '../../../../server/lib/permissions';
|
import { hasPermission, Permission } from '../../../../server/lib/permissions';
|
||||||
import useSettings from '../../../hooks/useSettings';
|
import useSettings from '../../../hooks/useSettings';
|
||||||
import { useUser } from '../../../hooks/useUser';
|
import { useUser } from '../../../hooks/useUser';
|
||||||
@@ -10,6 +11,7 @@ import Error from '../../../pages/_error';
|
|||||||
import Alert from '../../Common/Alert';
|
import Alert from '../../Common/Alert';
|
||||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
import PageTitle from '../../Common/PageTitle';
|
import PageTitle from '../../Common/PageTitle';
|
||||||
|
import SettingsTabs, { SettingsRoute } from '../../Common/SettingsTabs';
|
||||||
import ProfileHeader from '../ProfileHeader';
|
import ProfileHeader from '../ProfileHeader';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -21,21 +23,15 @@ const messages = defineMessages({
|
|||||||
"You do not have permission to modify this user's settings.",
|
"You do not have permission to modify this user's settings.",
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SettingsRoute {
|
|
||||||
text: string;
|
|
||||||
route: string;
|
|
||||||
regex: RegExp;
|
|
||||||
requiredPermission?: Permission | Permission[];
|
|
||||||
permissionType?: { type: 'and' | 'or' };
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSettings: React.FC = ({ children }) => {
|
const UserSettings: React.FC = ({ children }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { user: currentUser } = useUser();
|
const { user: currentUser } = useUser();
|
||||||
const { user, error } = useUser({ id: Number(router.query.userId) });
|
const { user, error } = useUser({ id: Number(router.query.userId) });
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { data } = useSWR<UserSettingsNotificationsResponse>(
|
||||||
|
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||||
|
);
|
||||||
|
|
||||||
if (!user && !error) {
|
if (!user && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -67,7 +63,9 @@ const UserSettings: React.FC = ({ children }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: intl.formatMessage(messages.menuNotifications),
|
text: intl.formatMessage(messages.menuNotifications),
|
||||||
route: '/settings/notifications',
|
route: data?.emailEnabled
|
||||||
|
? '/settings/notifications/email'
|
||||||
|
: '/settings/notifications/discord',
|
||||||
regex: /\/settings\/notifications/,
|
regex: /\/settings\/notifications/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,38 +77,6 @@ const UserSettings: React.FC = ({ children }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const activeLinkColor =
|
|
||||||
'border-indigo-600 text-indigo-500 focus:outline-none focus:text-indigo-500 focus:border-indigo-500';
|
|
||||||
|
|
||||||
const inactiveLinkColor =
|
|
||||||
'border-transparent text-gray-500 hover:text-gray-400 hover:border-gray-300 focus:outline-none focus:text-gray-4700 focus:border-gray-300';
|
|
||||||
|
|
||||||
const SettingsLink: React.FC<{
|
|
||||||
route: string;
|
|
||||||
regex: RegExp;
|
|
||||||
isMobile?: boolean;
|
|
||||||
}> = ({ children, route, regex, isMobile = false }) => {
|
|
||||||
const finalRoute = router.asPath.includes('/profile')
|
|
||||||
? `/profile${route}`
|
|
||||||
: `/users/${user.id}${route}`;
|
|
||||||
if (isMobile) {
|
|
||||||
return <option value={finalRoute}>{children}</option>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={finalRoute}>
|
|
||||||
<a
|
|
||||||
className={`whitespace-nowrap ml-8 first:ml-0 py-4 px-1 border-b-2 border-transparent font-medium text-sm leading-5 ${
|
|
||||||
router.pathname.match(regex) ? activeLinkColor : inactiveLinkColor
|
|
||||||
}`}
|
|
||||||
aria-current="page"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentUser?.id !== 1 && user.id === 1) {
|
if (currentUser?.id !== 1 && user.id === 1) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -133,13 +99,11 @@ const UserSettings: React.FC = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentRoute = settingsRoutes.find(
|
settingsRoutes.forEach((settingsRoute) => {
|
||||||
(route) => !!router.pathname.match(route.regex)
|
settingsRoute.route = router.asPath.includes('/profile')
|
||||||
)?.route;
|
? `/profile${settingsRoute.route}`
|
||||||
|
: `/users/${user.id}${settingsRoute.route}`;
|
||||||
const finalRoute = router.asPath.includes('/profile')
|
});
|
||||||
? `/profile${currentRoute}`
|
|
||||||
: `/users/${user.id}${currentRoute}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -151,68 +115,7 @@ const UserSettings: React.FC = ({ children }) => {
|
|||||||
/>
|
/>
|
||||||
<ProfileHeader user={user} isSettingsPage />
|
<ProfileHeader user={user} isSettingsPage />
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="sm:hidden">
|
<SettingsTabs settingsRoutes={settingsRoutes} />
|
||||||
<select
|
|
||||||
onChange={(e) => {
|
|
||||||
router.push(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
router.push(e.target.value);
|
|
||||||
}}
|
|
||||||
defaultValue={finalRoute}
|
|
||||||
aria-label="Selected tab"
|
|
||||||
>
|
|
||||||
{settingsRoutes
|
|
||||||
.filter(
|
|
||||||
(route) =>
|
|
||||||
!route.hidden &&
|
|
||||||
(route.requiredPermission
|
|
||||||
? hasPermission(
|
|
||||||
route.requiredPermission,
|
|
||||||
currentUser?.permissions ?? 0,
|
|
||||||
route.permissionType
|
|
||||||
)
|
|
||||||
: true)
|
|
||||||
)
|
|
||||||
.map((route, index) => (
|
|
||||||
<SettingsLink
|
|
||||||
route={route.route}
|
|
||||||
regex={route.regex}
|
|
||||||
isMobile
|
|
||||||
key={`mobile-settings-link-${index}`}
|
|
||||||
>
|
|
||||||
{route.text}
|
|
||||||
</SettingsLink>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:block">
|
|
||||||
<div className="border-b border-gray-600">
|
|
||||||
<nav className="flex -mb-px">
|
|
||||||
{settingsRoutes
|
|
||||||
.filter(
|
|
||||||
(route) =>
|
|
||||||
!route.hidden &&
|
|
||||||
(route.requiredPermission
|
|
||||||
? hasPermission(
|
|
||||||
route.requiredPermission,
|
|
||||||
currentUser?.permissions ?? 0,
|
|
||||||
route.permissionType
|
|
||||||
)
|
|
||||||
: true)
|
|
||||||
)
|
|
||||||
.map((route, index) => (
|
|
||||||
<SettingsLink
|
|
||||||
route={route.route}
|
|
||||||
regex={route.regex}
|
|
||||||
key={`standard-settings-link-${index}`}
|
|
||||||
>
|
|
||||||
{route.text}
|
|
||||||
</SettingsLink>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 text-white">{children}</div>
|
<div className="mt-10 text-white">{children}</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces';
|
||||||
|
|
||||||
export interface SettingsContextProps {
|
export interface SettingsContextProps {
|
||||||
currentSettings: PublicSettingsResponse;
|
currentSettings: PublicSettingsResponse;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
enableNotifications: boolean;
|
|
||||||
discordId?: string;
|
discordId?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
originalLanguage?: string;
|
originalLanguage?: string;
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
"components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when requested media fails to be added to Radarr or Sonarr.",
|
"components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when requested media fails to be added to Radarr or Sonarr.",
|
||||||
"components.NotificationTypeSelector.mediarequested": "Media Requested",
|
"components.NotificationTypeSelector.mediarequested": "Media Requested",
|
||||||
"components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when media is requested and requires approval.",
|
"components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when media is requested and requires approval.",
|
||||||
|
"components.NotificationTypeSelector.notificationTypes": "Notification Types",
|
||||||
"components.PermissionEdit.admin": "Admin",
|
"components.PermissionEdit.admin": "Admin",
|
||||||
"components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.",
|
"components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.",
|
||||||
"components.PermissionEdit.advancedrequest": "Advanced Requests",
|
"components.PermissionEdit.advancedrequest": "Advanced Requests",
|
||||||
@@ -243,41 +244,37 @@
|
|||||||
"components.Search.searchresults": "Search Results",
|
"components.Search.searchresults": "Search Results",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.accessToken": "Access Token",
|
"components.Settings.Notifications.NotificationsPushbullet.accessToken": "Access Token",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "Enable Agent",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.notificationTypes": "Notification Types",
|
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Pushbullet notification settings failed to save.",
|
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Pushbullet notification settings failed to save.",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Pushbullet notification settings saved successfully!",
|
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Pushbullet notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.settingUpPushbullet": "Setting Up Pushbullet Notifications",
|
"components.Settings.Notifications.NotificationsPushbullet.settingUpPushbullet": "Setting Up Pushbullet Notifications",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.settingUpPushbulletDescription": "To configure Pushbullet notifications, you will need to <CreateAccessTokenLink>create an access token</CreateAccessTokenLink> and enter it below.",
|
"components.Settings.Notifications.NotificationsPushbullet.settingUpPushbulletDescription": "To configure Pushbullet notifications, you will need to <CreateAccessTokenLink>create an access token</CreateAccessTokenLink>.",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.testSent": "Test notification sent!",
|
"components.Settings.Notifications.NotificationsPushbullet.testSent": "Pushbullet test notification sent!",
|
||||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token",
|
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token",
|
||||||
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application/API Token",
|
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application/API Token",
|
||||||
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
|
||||||
"components.Settings.Notifications.NotificationsPushover.notificationtypes": "Notification Types",
|
|
||||||
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.",
|
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.",
|
||||||
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notification settings saved successfully!",
|
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.NotificationsPushover.settinguppushover": "Setting Up Pushover Notifications",
|
"components.Settings.Notifications.NotificationsPushover.settinguppushover": "Setting Up Pushover Notifications",
|
||||||
"components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "To configure Pushover notifications, you will need to <RegisterApplicationLink>register an application</RegisterApplicationLink> and enter the API token below. (You can use one of our <IconLink>official icons on GitHub</IconLink>.) You will also need your user key.",
|
"components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "To configure Pushover notifications, you will need to <RegisterApplicationLink>register an application</RegisterApplicationLink> and enter the API token below. (You can use one of the <IconLink>official Overseerr icons on GitHub</IconLink>.)",
|
||||||
"components.Settings.Notifications.NotificationsPushover.testsent": "Test notification sent!",
|
"components.Settings.Notifications.NotificationsPushover.testsent": "Pushover test notification sent!",
|
||||||
"components.Settings.Notifications.NotificationsPushover.userToken": "User Key",
|
"components.Settings.Notifications.NotificationsPushover.userToken": "User or Group Key",
|
||||||
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide a valid application token",
|
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide a valid application token",
|
||||||
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user key",
|
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user key",
|
||||||
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Enable Agent",
|
||||||
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Notification Types",
|
|
||||||
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Setting Up Slack Notifications",
|
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Setting Up Slack Notifications",
|
||||||
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "To configure Slack notifications, you will need to create an <WebhookLink>Incoming Webhook</WebhookLink> integration and enter the webhook URL below.",
|
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "To configure Slack notifications, you will need to create an <WebhookLink>Incoming Webhook</WebhookLink> integration and enter the webhook URL below.",
|
||||||
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notification settings failed to save.",
|
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notification settings failed to save.",
|
||||||
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notification settings saved successfully!",
|
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.NotificationsSlack.testsent": "Test notification sent!",
|
"components.Settings.Notifications.NotificationsSlack.testsent": "Slack test notification sent!",
|
||||||
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Enable Agent",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.authheader": "Authorization Header",
|
"components.Settings.Notifications.NotificationsWebhook.authheader": "Authorization Header",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
|
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.notificationtypes": "Notification Types",
|
|
||||||
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default",
|
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!",
|
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Template Variable Help",
|
"components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Template Variable Help",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.testsent": "Test notification sent!",
|
"components.Settings.Notifications.NotificationsWebhook.testsent": "Webhook test notification sent!",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "You must provide a valid JSON payload",
|
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "You must provide a valid JSON payload",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL",
|
||||||
@@ -290,6 +287,7 @@
|
|||||||
"components.Settings.Notifications.botAPI": "Bot Authentication Token",
|
"components.Settings.Notifications.botAPI": "Bot Authentication Token",
|
||||||
"components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL",
|
"components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL",
|
||||||
"components.Settings.Notifications.botUsername": "Bot Username",
|
"components.Settings.Notifications.botUsername": "Bot Username",
|
||||||
|
"components.Settings.Notifications.botUsernameTip": "Allow users to start a chat with the bot and configure their own personal notifications",
|
||||||
"components.Settings.Notifications.chatId": "Chat ID",
|
"components.Settings.Notifications.chatId": "Chat ID",
|
||||||
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
|
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
|
||||||
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!",
|
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!",
|
||||||
@@ -300,11 +298,10 @@
|
|||||||
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
|
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
|
||||||
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!",
|
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.enableSsl": "Enable SSL",
|
"components.Settings.Notifications.enableSsl": "Enable SSL",
|
||||||
"components.Settings.Notifications.notificationtypes": "Notification Types",
|
"components.Settings.Notifications.pgpPassword": "PGP Password",
|
||||||
"components.Settings.Notifications.pgpPassword": "<PgpLink>PGP</PgpLink> Password",
|
"components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>",
|
||||||
"components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages (PGP private key is also required)",
|
"components.Settings.Notifications.pgpPrivateKey": "PGP Private Key",
|
||||||
"components.Settings.Notifications.pgpPrivateKey": "<PgpLink>PGP</PgpLink> Private Key",
|
"components.Settings.Notifications.pgpPrivateKeyTip": "Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>",
|
||||||
"components.Settings.Notifications.pgpPrivateKeyTip": "Sign encrypted email messages (PGP password is also required)",
|
|
||||||
"components.Settings.Notifications.sendSilently": "Send Silently",
|
"components.Settings.Notifications.sendSilently": "Send Silently",
|
||||||
"components.Settings.Notifications.sendSilentlyTip": "Send notifications with no sound",
|
"components.Settings.Notifications.sendSilentlyTip": "Send notifications with no sound",
|
||||||
"components.Settings.Notifications.senderName": "Sender Name",
|
"components.Settings.Notifications.senderName": "Sender Name",
|
||||||
@@ -315,11 +312,13 @@
|
|||||||
"components.Settings.Notifications.ssldisabletip": "SSL should be disabled on standard TLS connections (port 587)",
|
"components.Settings.Notifications.ssldisabletip": "SSL should be disabled on standard TLS connections (port 587)",
|
||||||
"components.Settings.Notifications.telegramsettingsfailed": "Telegram notification settings failed to save.",
|
"components.Settings.Notifications.telegramsettingsfailed": "Telegram notification settings failed to save.",
|
||||||
"components.Settings.Notifications.telegramsettingssaved": "Telegram notification settings saved successfully!",
|
"components.Settings.Notifications.telegramsettingssaved": "Telegram notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.testsent": "Test notification sent!",
|
"components.Settings.Notifications.testsent": "Telegram test notification sent!",
|
||||||
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authentication token",
|
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authentication token",
|
||||||
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
|
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
|
||||||
"components.Settings.Notifications.validationEmail": "You must provide a valid email address",
|
"components.Settings.Notifications.validationEmail": "You must provide a valid email address",
|
||||||
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide a hostname or IP 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.validationSmtpHostRequired": "You must provide a valid hostname or IP address",
|
||||||
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide a valid port number",
|
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide a valid port number",
|
||||||
"components.Settings.Notifications.validationUrl": "You must provide a valid URL",
|
"components.Settings.Notifications.validationUrl": "You must provide a valid URL",
|
||||||
"components.Settings.Notifications.webhookUrl": "Webhook URL",
|
"components.Settings.Notifications.webhookUrl": "Webhook URL",
|
||||||
@@ -524,7 +523,6 @@
|
|||||||
"components.Settings.default4k": "Default 4K",
|
"components.Settings.default4k": "Default 4K",
|
||||||
"components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?",
|
"components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?",
|
||||||
"components.Settings.email": "Email",
|
"components.Settings.email": "Email",
|
||||||
"components.Settings.enablenotifications": "Enable Notifications",
|
|
||||||
"components.Settings.enablessl": "Enable SSL",
|
"components.Settings.enablessl": "Enable SSL",
|
||||||
"components.Settings.general": "General",
|
"components.Settings.general": "General",
|
||||||
"components.Settings.generalsettings": "General Settings",
|
"components.Settings.generalsettings": "General Settings",
|
||||||
@@ -544,11 +542,9 @@
|
|||||||
"components.Settings.menuUsers": "Users",
|
"components.Settings.menuUsers": "Users",
|
||||||
"components.Settings.nodefault": "No Default Server",
|
"components.Settings.nodefault": "No Default Server",
|
||||||
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
||||||
"components.Settings.notificationAgentSettingsDescription": "Choose the types of notifications to send, and which notification agents to use.",
|
"components.Settings.notificationAgentSettingsDescription": "Configure and enable notification agents.",
|
||||||
"components.Settings.notificationAgentsSettings": "Notification Agents",
|
|
||||||
"components.Settings.notifications": "Notifications",
|
"components.Settings.notifications": "Notifications",
|
||||||
"components.Settings.notificationsettings": "Notification Settings",
|
"components.Settings.notificationsettings": "Notification Settings",
|
||||||
"components.Settings.notificationsettingsDescription": "Configure global notification settings. The options below will apply to all notification agents.",
|
|
||||||
"components.Settings.notificationsettingsfailed": "Notification settings failed to save.",
|
"components.Settings.notificationsettingsfailed": "Notification settings failed to save.",
|
||||||
"components.Settings.notificationsettingssaved": "Notification settings saved successfully!",
|
"components.Settings.notificationsettingssaved": "Notification settings saved successfully!",
|
||||||
"components.Settings.notrunning": "Not Running",
|
"components.Settings.notrunning": "Not Running",
|
||||||
@@ -715,22 +711,31 @@
|
|||||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
|
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
|
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
|
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Discord ID",
|
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your Discord user account",
|
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your user account",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.enableNotifications": "Enable Notifications",
|
"components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Discord notification settings failed to save.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord notification settings saved successfully!",
|
||||||
|
"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.notifications": "Notifications",
|
"components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings",
|
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.pgpKey": "<PgpLink>PGP</PgpLink> Public Key",
|
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.pgpKeyTip": "Encrypt email messages",
|
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Encrypt email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Telegram Messages Silently",
|
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Silently",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Send notifications with no sound",
|
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Send notifications with no sound",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Telegram Chat ID",
|
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chat ID",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTip": "Add <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat",
|
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command",
|
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram notification settings failed to save.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram notification settings saved successfully!",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsFailure": "Something went wrong while saving settings.",
|
"components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsFailure": "Something went wrong while saving settings.",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsSuccess": "Notification settings saved successfully!",
|
"components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsSuccess": "Notification settings saved successfully!",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid Discord user ID",
|
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid user ID",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid Telegram chat ID",
|
"components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "You must provide a valid PGP public key",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID",
|
||||||
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password",
|
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password",
|
||||||
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password",
|
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password",
|
||||||
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",
|
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { NextPage } from 'next';
|
|
||||||
import React from 'react';
|
|
||||||
import UserSettings from '../../../components/UserProfile/UserSettings';
|
|
||||||
import UserNotificationSettings from '../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
|
||||||
|
|
||||||
const UserSettingsMainPage: NextPage = () => {
|
|
||||||
return (
|
|
||||||
<UserSettings>
|
|
||||||
<UserNotificationSettings />
|
|
||||||
</UserSettings>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserSettingsMainPage;
|
|
||||||
17
src/pages/profile/settings/notifications/discord.tsx
Normal file
17
src/pages/profile/settings/notifications/discord.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { NextPage } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
import UserSettings from '../../../../components/UserProfile/UserSettings';
|
||||||
|
import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||||
|
import UserNotificationsDiscord from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord';
|
||||||
|
|
||||||
|
const NotificationsPage: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<UserSettings>
|
||||||
|
<UserNotificationSettings>
|
||||||
|
<UserNotificationsDiscord />
|
||||||
|
</UserNotificationSettings>
|
||||||
|
</UserSettings>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
17
src/pages/profile/settings/notifications/email.tsx
Normal file
17
src/pages/profile/settings/notifications/email.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { NextPage } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
import UserSettings from '../../../../components/UserProfile/UserSettings';
|
||||||
|
import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||||
|
import UserNotificationsEmail from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail';
|
||||||
|
|
||||||
|
const NotificationsPage: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<UserSettings>
|
||||||
|
<UserNotificationSettings>
|
||||||
|
<UserNotificationsEmail />
|
||||||
|
</UserNotificationSettings>
|
||||||
|
</UserSettings>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
17
src/pages/profile/settings/notifications/telegram.tsx
Normal file
17
src/pages/profile/settings/notifications/telegram.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { NextPage } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
import UserSettings from '../../../../components/UserProfile/UserSettings';
|
||||||
|
import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||||
|
import UserNotificationsTelegram from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram';
|
||||||
|
|
||||||
|
const NotificationsPage: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<UserSettings>
|
||||||
|
<UserNotificationSettings>
|
||||||
|
<UserNotificationsTelegram />
|
||||||
|
</UserNotificationSettings>
|
||||||
|
</UserSettings>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { NextPage } from 'next';
|
|
||||||
import React from 'react';
|
|
||||||
import UserSettings from '../../../../components/UserProfile/UserSettings';
|
|
||||||
import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
|
||||||
import useRouteGuard from '../../../../hooks/useRouteGuard';
|
|
||||||
import { Permission } from '../../../../hooks/useUser';
|
|
||||||
|
|
||||||
const UserSettingsMainPage: NextPage = () => {
|
|
||||||
useRouteGuard(Permission.MANAGE_USERS);
|
|
||||||
return (
|
|
||||||
<UserSettings>
|
|
||||||
<UserNotificationSettings />
|
|
||||||
</UserSettings>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserSettingsMainPage;
|
|
||||||
20
src/pages/users/[userId]/settings/notifications/discord.tsx
Normal file
20
src/pages/users/[userId]/settings/notifications/discord.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextPage } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
import UserSettings from '../../../../../components/UserProfile/UserSettings';
|
||||||
|
import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||||
|
import UserNotificationsDiscord from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord';
|
||||||
|
import useRouteGuard from '../../../../../hooks/useRouteGuard';
|
||||||
|
import { Permission } from '../../../../../hooks/useUser';
|
||||||
|
|
||||||
|
const NotificationsPage: NextPage = () => {
|
||||||
|
useRouteGuard(Permission.MANAGE_USERS);
|
||||||
|
return (
|
||||||
|
<UserSettings>
|
||||||
|
<UserNotificationSettings>
|
||||||
|
<UserNotificationsDiscord />
|
||||||
|
</UserNotificationSettings>
|
||||||
|
</UserSettings>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
20
src/pages/users/[userId]/settings/notifications/email.tsx
Normal file
20
src/pages/users/[userId]/settings/notifications/email.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextPage } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
import UserSettings from '../../../../../components/UserProfile/UserSettings';
|
||||||
|
import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||||
|
import UserNotificationsEmail from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail';
|
||||||
|
import useRouteGuard from '../../../../../hooks/useRouteGuard';
|
||||||
|
import { Permission } from '../../../../../hooks/useUser';
|
||||||
|
|
||||||
|
const NotificationsPage: NextPage = () => {
|
||||||
|
useRouteGuard(Permission.MANAGE_USERS);
|
||||||
|
return (
|
||||||
|
<UserSettings>
|
||||||
|
<UserNotificationSettings>
|
||||||
|
<UserNotificationsEmail />
|
||||||
|
</UserNotificationSettings>
|
||||||
|
</UserSettings>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
20
src/pages/users/[userId]/settings/notifications/telegram.tsx
Normal file
20
src/pages/users/[userId]/settings/notifications/telegram.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextPage } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
import UserSettings from '../../../../../components/UserProfile/UserSettings';
|
||||||
|
import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||||
|
import UserNotificationsTelegram from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram';
|
||||||
|
import useRouteGuard from '../../../../../hooks/useRouteGuard';
|
||||||
|
import { Permission } from '../../../../../hooks/useUser';
|
||||||
|
|
||||||
|
const NotificationsPage: NextPage = () => {
|
||||||
|
useRouteGuard(Permission.MANAGE_USERS);
|
||||||
|
return (
|
||||||
|
<UserSettings>
|
||||||
|
<UserNotificationSettings>
|
||||||
|
<UserNotificationsTelegram />
|
||||||
|
</UserNotificationSettings>
|
||||||
|
</UserSettings>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
@@ -217,14 +217,6 @@ img.avatar-sm {
|
|||||||
@apply flex max-w-lg rounded-md shadow-sm;
|
@apply flex max-w-lg rounded-md shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-required {
|
|
||||||
@apply text-red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-tip {
|
|
||||||
@apply block text-gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@apply pt-5 mt-8 text-white border-t border-gray-700;
|
@apply pt-5 mt-8 text-white border-t border-gray-700;
|
||||||
}
|
}
|
||||||
@@ -241,6 +233,18 @@ label.text-label {
|
|||||||
@apply sm:mt-2;
|
@apply sm:mt-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label a {
|
||||||
|
@apply text-gray-100 transition duration-300 hover:text-white hover:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-required {
|
||||||
|
@apply ml-1 text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-tip {
|
||||||
|
@apply block text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
|
|||||||
Reference in New Issue
Block a user