From 1b1645846f18ef59bf2f25aeb519719f4b236bc4 Mon Sep 17 00:00:00 2001 From: haorwen Date: Sun, 18 Jan 2026 22:52:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E8=AE=BE=E7=BD=AE=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 增加消息推送设置 * chore: bump version to v0.9.52 --- package.json | 2 +- pnpm-lock.yaml | 17 +- public/locales/de/setting.json | 62 ++- public/locales/en/setting.json | 60 ++- public/locales/es/setting.json | 60 ++- public/locales/fr/setting.json | 60 ++- public/locales/jp/setting.json | 61 ++- public/locales/pt/setting.json | 60 ++- public/locales/ru/setting.json | 60 ++- public/locales/tr/setting.json | 60 ++- public/locales/zh/setting.json | 60 ++- src/app/services/notification.ts | 89 ++++ src/app/store.ts | 7 +- .../channelSchemas.ts | 310 +++++++++++++ .../AdminNotificationChannels/index.tsx | 224 +++++++++ src/routes/setting/NotificationSettings.tsx | 430 ++++++++++++++++++ src/routes/setting/navs.tsx | 11 + src/types/notification.ts | 101 ++++ 18 files changed, 1710 insertions(+), 24 deletions(-) create mode 100644 src/app/services/notification.ts create mode 100644 src/routes/setting/AdminNotificationChannels/channelSchemas.ts create mode 100644 src/routes/setting/AdminNotificationChannels/index.tsx create mode 100644 src/routes/setting/NotificationSettings.tsx create mode 100644 src/types/notification.ts diff --git a/package.json b/package.json index 73df65f2..7300697c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vocechat-web", - "version": "0.9.51", + "version": "0.9.52", "homepage": "https://voce.chat", "dependencies": { "@metamask/onboarding": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2811369..59f26feb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2604,11 +2604,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001718: - resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} - - caniuse-lite@1.0.30001731: - resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} + caniuse-lite@1.0.30001764: + resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -8773,7 +8770,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.25.1 - caniuse-lite: 1.0.30001718 + caniuse-lite: 1.0.30001764 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -8921,7 +8918,7 @@ snapshots: browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001731 + caniuse-lite: 1.0.30001764 electron-to-chromium: 1.5.199 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -8961,13 +8958,11 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.25.1 - caniuse-lite: 1.0.30001718 + caniuse-lite: 1.0.30001764 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001718: {} - - caniuse-lite@1.0.30001731: {} + caniuse-lite@1.0.30001764: {} chalk@4.1.2: dependencies: diff --git a/public/locales/de/setting.json b/public/locales/de/setting.json index 26897795..aad62e8d 100644 --- a/public/locales/de/setting.json +++ b/public/locales/de/setting.json @@ -13,6 +13,7 @@ "my_account": "Mein Konto", "config": "Konfiguration", "bot": "Bot & Webhook", + "notification_channels": "Benachrichtigungskanäle", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -23,7 +24,8 @@ "about": "Über", "api_doc": "API-Dokumentation", "version": "Version", - "video": "Videoanrufe" + "video": "Videoanrufe", + "notification": "Benachrichtigungen" }, "vocespace": { "add": "Hinzufügen", @@ -350,6 +352,62 @@ "step_1": "Schritt 1: Eigene Server-URL eintragen", "step_2": "Schritt 2: Login-Token eingeben", "step_2_desc": "Token per X-API-Key-Header abrufen oder unten kopieren:", - "last": "Jede API hat einen „Testen“-Button zur Debugging." + "last": "Jede API hat einen „Testen"-Button zur Debugging." + }, + "notification": { + "desc": "Benachrichtigungseinstellungen für verschiedene Kanäle konfigurieren", + "loading": "Lädt...", + "no_channels": "Keine Benachrichtigungskanäle verfügbar. Bitte kontaktieren Sie Ihren Administrator, um Benachrichtigungskanäle zu konfigurieren.", + "status": "Status", + "enabled": "Aktiviert", + "not_configured": "Nicht konfiguriert", + "configure": "Konfigurieren", + "enable_notifications": "Benachrichtigungen aktivieren", + "trigger_conditions": "Auslösebedingungen", + "trigger_on_all_messages": "Alle Nachrichten empfangen", + "trigger_on_mentions": "Bei Erwähnung benachrichtigen", + "skip_when_online": "Benachrichtigungen überspringen, wenn online", + "select_groups": "Gruppen auswählen", + "save": "Speichern", + "saving": "Speichert...", + "cancel": "Abbrechen", + "delete": "Konfiguration löschen", + "deleting": "Löscht...", + "save_success": "Benachrichtigungseinstellungen erfolgreich gespeichert", + "save_error": "Fehler beim Speichern der Benachrichtigungseinstellungen", + "delete_success": "Benachrichtigungseinstellungen erfolgreich gelöscht", + "delete_error": "Fehler beim Löschen der Benachrichtigungseinstellungen" + }, + "admin_notification_channels": { + "desc": "Benachrichtigungskanäle für Benutzer konfigurieren", + "create_channel": "Kanal erstellen", + "edit_channel": "Bearbeiten", + "delete_channel": "Löschen", + "test_channel": "Testen", + "channel_name": "Kanalname", + "channel_type": "Kanaltyp", + "channel_status": "Status", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "actions": "Aktionen", + "create_title": "Benachrichtigungskanal erstellen", + "create_desc": "Neuen Benachrichtigungskanal konfigurieren", + "edit_title": "Benachrichtigungskanal bearbeiten", + "edit_desc": "Kanalkonfiguration aktualisieren", + "delete_title": "Kanal löschen", + "delete_desc": "Möchten Sie diesen Kanal wirklich löschen? Alle Benutzereinstellungen für diesen Kanal werden entfernt.", + "test_success": "Testbenachrichtigung erfolgreich gesendet", + "test_error": "Fehler beim Senden der Testbenachrichtigung", + "create_success": "Kanal erfolgreich erstellt", + "create_error": "Fehler beim Erstellen des Kanals", + "update_success": "Kanal erfolgreich aktualisiert", + "update_error": "Fehler beim Aktualisieren des Kanals", + "delete_success": "Kanal erfolgreich gelöscht", + "delete_error": "Fehler beim Löschen des Kanals", + "config_section": "Kanalkonfiguration", + "basic_info": "Grundinformationen", + "enable_channel": "Kanal aktivieren", + "rate_limit": "Ratenlimit (Anfragen pro Sekunde)", + "no_channels": "Noch keine Benachrichtigungskanäle konfiguriert" } } diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json index 36e0acdf..1040a329 100644 --- a/public/locales/en/setting.json +++ b/public/locales/en/setting.json @@ -13,6 +13,7 @@ "my_account": "My Account", "config": "Configuration", "bot": "Bot & Webhook", + "notification_channels": "Notification Channels", "firebase": "Firebase", "agora": "Agora", "vocespace": "Video", @@ -24,7 +25,8 @@ "about": "About", "api_doc": "API Documentation", "version": "Version", - "video": "Video" + "video": "Video", + "notification": "Notifications" }, "channel": { "leave": "Leave Channel", @@ -379,5 +381,61 @@ "step_2": "Step Two: Input the login token", "step_2_desc": "You can get the token by access X-API-Key http header or just copy the text below:", "last": "Every API has a 'Try it Out' button, and you can debug the API with it" + }, + "notification": { + "desc": "Configure your notification channels", + "loading": "Loading...", + "no_channels": "No notification channel types available. Please contact your administrator.", + "status": "Status", + "enabled": "Enabled", + "disabled": "Disabled", + "my_channels": "My Notification Channels", + "available_types": "Available Channel Types", + "create_channel": "Create Channel", + "edit_channel": "Edit Channel", + "channel_name": "Channel Name", + "channel_config": "Channel Configuration", + "enable_notifications": "Enable notifications", + "trigger_conditions": "Trigger conditions", + "trigger_on_all_messages": "Receive all messages", + "trigger_on_mentions": "Notify when mentioned", + "skip_when_online": "Skip notifications when online", + "select_groups": "Select groups", + "save": "Save", + "saving": "Saving...", + "cancel": "Cancel", + "edit": "Edit", + "test": "Test", + "delete": "Delete", + "save_success": "Channel saved successfully", + "save_error": "Failed to save channel", + "create_success": "Channel created successfully", + "create_error": "Failed to create channel", + "delete_success": "Channel deleted successfully", + "delete_error": "Failed to delete channel", + "test_success": "Test notification sent successfully", + "test_error": "Failed to send test notification" + }, + "admin_notification_channels": { + "desc": "Enable notification channel types for users", + "enabled_types": "Enabled Channel Types", + "available_types": "Available Channel Types", + "enable_type": "Enable", + "channel_type": "Channel Type", + "channel_status": "Status", + "enabled": "Enabled", + "disabled": "Disabled", + "actions": "Actions", + "delete_channel": "Delete", + "delete_title": "Delete Channel Type", + "delete_desc": "Are you sure you want to remove this channel type? Users will no longer be able to create new channels of this type.", + "cancel": "Cancel", + "delete": "Delete", + "create_success": "Channel type enabled successfully", + "create_error": "Failed to enable channel type", + "update_success": "Channel type updated successfully", + "update_error": "Failed to update channel type", + "delete_success": "Channel type deleted successfully", + "delete_error": "Failed to delete channel type" } } diff --git a/public/locales/es/setting.json b/public/locales/es/setting.json index 947cb6ba..3c053f60 100644 --- a/public/locales/es/setting.json +++ b/public/locales/es/setting.json @@ -13,6 +13,7 @@ "my_account": "Mi Cuenta", "config": "Configuración", "bot": "Bot & Webhook", + "notification_channels": "Canales de Notificación", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -23,7 +24,8 @@ "about": "Acerca de", "api_doc": "Documentación de API", "version": "Versión", - "video": "Video" + "video": "Video", + "notification": "Notificaciones" }, "vocespace": { "add": "Agregar", @@ -293,5 +295,61 @@ "step_2": "Paso Dos: Ingresa el token de inicio de sesión", "step_2_desc": "Puedes obtener el token accediendo al encabezado X-API-Key http o simplemente copiando el texto a continuación:", "last": "Cada API tiene un botón 'Probar' para que puedas depurar la API con él" + }, + "notification": { + "desc": "Configurar preferencias de notificación para diferentes canales", + "loading": "Cargando...", + "no_channels": "No hay canales de notificación disponibles. Póngase en contacto con su administrador para configurar los canales de notificación.", + "status": "Estado", + "enabled": "Habilitado", + "not_configured": "No configurado", + "configure": "Configurar", + "enable_notifications": "Habilitar notificaciones", + "trigger_conditions": "Condiciones de activación", + "trigger_on_all_messages": "Recibir todos los mensajes", + "trigger_on_mentions": "Notificar cuando se mencione", + "skip_when_online": "Omitir notificaciones cuando esté en línea", + "select_groups": "Seleccionar grupos", + "save": "Guardar", + "saving": "Guardando...", + "cancel": "Cancelar", + "delete": "Eliminar configuración", + "deleting": "Eliminando...", + "save_success": "Preferencias de notificación guardadas correctamente", + "save_error": "Error al guardar las preferencias de notificación", + "delete_success": "Preferencias de notificación eliminadas correctamente", + "delete_error": "Error al eliminar las preferencias de notificación" + }, + "admin_notification_channels": { + "desc": "Configurar canales de notificación para usuarios", + "create_channel": "Crear Canal", + "edit_channel": "Editar", + "delete_channel": "Eliminar", + "test_channel": "Probar", + "channel_name": "Nombre del Canal", + "channel_type": "Tipo de Canal", + "channel_status": "Estado", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "actions": "Acciones", + "create_title": "Crear Canal de Notificación", + "create_desc": "Configurar un nuevo canal de notificación", + "edit_title": "Editar Canal de Notificación", + "edit_desc": "Actualizar configuración del canal", + "delete_title": "Eliminar Canal", + "delete_desc": "¿Está seguro de que desea eliminar este canal? Se eliminarán todas las preferencias de usuario para este canal.", + "test_success": "Notificación de prueba enviada correctamente", + "test_error": "Error al enviar la notificación de prueba", + "create_success": "Canal creado correctamente", + "create_error": "Error al crear el canal", + "update_success": "Canal actualizado correctamente", + "update_error": "Error al actualizar el canal", + "delete_success": "Canal eliminado correctamente", + "delete_error": "Error al eliminar el canal", + "config_section": "Configuración del Canal", + "basic_info": "Información Básica", + "enable_channel": "Habilitar Canal", + "rate_limit": "Límite de velocidad (solicitudes por segundo)", + "no_channels": "Aún no se han configurado canales de notificación" } } diff --git a/public/locales/fr/setting.json b/public/locales/fr/setting.json index 7fc5d25f..ece86c7d 100644 --- a/public/locales/fr/setting.json +++ b/public/locales/fr/setting.json @@ -13,6 +13,7 @@ "my_account": "Mon Compte", "config": "Configuration", "bot": "Bot & Webhook", + "notification_channels": "Canaux de Notification", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -23,7 +24,8 @@ "about": "À Propos", "api_doc": "Documentation API", "version": "Version", - "video": "Vidéo" + "video": "Vidéo", + "notification": "Notifications" }, "vocespace": { "add": "Ajouter", @@ -299,5 +301,61 @@ "step_2": "Étape Deux : Entrez le token de connexion", "step_2_desc": "Vous pouvez obtenir le token en accédant à l'en-tête http X-API-Key ou en copiant simplement le texte ci-dessous :", "last": "Chaque API a un bouton 'Essayez-le', et vous pouvez déboguer l'API avec cela" + }, + "notification": { + "desc": "Configurer les préférences de notification pour différents canaux", + "loading": "Chargement...", + "no_channels": "Aucun canal de notification disponible. Veuillez contacter votre administrateur pour configurer les canaux de notification.", + "status": "Statut", + "enabled": "Activé", + "not_configured": "Non configuré", + "configure": "Configurer", + "enable_notifications": "Activer les notifications", + "trigger_conditions": "Conditions de déclenchement", + "trigger_on_all_messages": "Recevoir tous les messages", + "trigger_on_mentions": "Notifier lors d'une mention", + "skip_when_online": "Ignorer les notifications en ligne", + "select_groups": "Sélectionner les groupes", + "save": "Enregistrer", + "saving": "Enregistrement...", + "cancel": "Annuler", + "delete": "Supprimer la configuration", + "deleting": "Suppression...", + "save_success": "Préférences de notification enregistrées avec succès", + "save_error": "Échec de l'enregistrement des préférences de notification", + "delete_success": "Préférences de notification supprimées avec succès", + "delete_error": "Échec de la suppression des préférences de notification" + }, + "admin_notification_channels": { + "desc": "Configurer les canaux de notification pour les utilisateurs", + "create_channel": "Créer un Canal", + "edit_channel": "Modifier", + "delete_channel": "Supprimer", + "test_channel": "Tester", + "channel_name": "Nom du Canal", + "channel_type": "Type de Canal", + "channel_status": "Statut", + "enabled": "Activé", + "disabled": "Désactivé", + "actions": "Actions", + "create_title": "Créer un Canal de Notification", + "create_desc": "Configurer un nouveau canal de notification", + "edit_title": "Modifier le Canal de Notification", + "edit_desc": "Mettre à jour la configuration du canal", + "delete_title": "Supprimer le Canal", + "delete_desc": "Êtes-vous sûr de vouloir supprimer ce canal ? Toutes les préférences utilisateur pour ce canal seront supprimées.", + "test_success": "Notification de test envoyée avec succès", + "test_error": "Échec de l'envoi de la notification de test", + "create_success": "Canal créé avec succès", + "create_error": "Échec de la création du canal", + "update_success": "Canal mis à jour avec succès", + "update_error": "Échec de la mise à jour du canal", + "delete_success": "Canal supprimé avec succès", + "delete_error": "Échec de la suppression du canal", + "config_section": "Configuration du Canal", + "basic_info": "Informations de Base", + "enable_channel": "Activer le Canal", + "rate_limit": "Limite de débit (requêtes par seconde)", + "no_channels": "Aucun canal de notification configuré pour le moment" } } diff --git a/public/locales/jp/setting.json b/public/locales/jp/setting.json index 75b64213..3de41271 100644 --- a/public/locales/jp/setting.json +++ b/public/locales/jp/setting.json @@ -9,6 +9,8 @@ "members": "メンバー", "my_account": "アカウント", "config": "設定", + "bot": "ボット&Webhook", + "notification_channels": "通知チャンネル", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -19,7 +21,8 @@ "about": "このアプリについて", "api_doc": "API ドキュメント", "version": "Version", - "video": "ビデオ" + "video": "ビデオ", + "notification": "通知設定" }, "vocespace": { "add": "追加", @@ -165,5 +168,61 @@ "step_2": "2. 登録済みのトークンを入力してください", "step_2_desc": "トークンを取得するには、http header の X-API-Key にアクセスしてください、もしくは以下のテキストを直接コピーで使用してください👇", "last": "3. 最後に各 API には Try it Out オプションが付いてる、あれを使って API を試すことができます" + }, + "notification": { + "desc": "異なるチャネルの通知設定を構成する", + "loading": "読み込み中...", + "no_channels": "利用可能な通知チャネルがありません。管理者に連絡して通知チャネルを設定してください。", + "status": "ステータス", + "enabled": "有効", + "not_configured": "未設定", + "configure": "設定", + "enable_notifications": "通知を有効にする", + "trigger_conditions": "トリガー条件", + "trigger_on_all_messages": "すべてのメッセージを受信", + "trigger_on_mentions": "メンション時に通知", + "skip_when_online": "オンライン時は通知をスキップ", + "select_groups": "グループを選択", + "save": "保存", + "saving": "保存中...", + "cancel": "キャンセル", + "delete": "設定を削除", + "deleting": "削除中...", + "save_success": "通知設定が正常に保存されました", + "save_error": "通知設定の保存に失敗しました", + "delete_success": "通知設定が正常に削除されました", + "delete_error": "通知設定の削除に失敗しました" + }, + "admin_notification_channels": { + "desc": "ユーザー向けの通知チャンネルを設定", + "create_channel": "チャンネルを作成", + "edit_channel": "編集", + "delete_channel": "削除", + "test_channel": "テスト", + "channel_name": "チャンネル名", + "channel_type": "チャンネルタイプ", + "channel_status": "ステータス", + "enabled": "有効", + "disabled": "無効", + "actions": "操作", + "create_title": "通知チャンネルを作成", + "create_desc": "新しい通知チャンネルを設定", + "edit_title": "通知チャンネルを編集", + "edit_desc": "チャンネル設定を更新", + "delete_title": "チャンネルを削除", + "delete_desc": "このチャンネルを削除してもよろしいですか?このチャンネルのすべてのユーザー設定が削除されます。", + "test_success": "テスト通知が正常に送信されました", + "test_error": "テスト通知の送信に失敗しました", + "create_success": "チャンネルが正常に作成されました", + "create_error": "チャンネルの作成に失敗しました", + "update_success": "チャンネルが正常に更新されました", + "update_error": "チャンネルの更新に失敗しました", + "delete_success": "チャンネルが正常に削除されました", + "delete_error": "チャンネルの削除に失敗しました", + "config_section": "チャンネル設定", + "basic_info": "基本情報", + "enable_channel": "チャンネルを有効にする", + "rate_limit": "レート制限(秒あたりのリクエスト数)", + "no_channels": "通知チャンネルがまだ設定されていません" } } diff --git a/public/locales/pt/setting.json b/public/locales/pt/setting.json index d34b996d..c57495dd 100644 --- a/public/locales/pt/setting.json +++ b/public/locales/pt/setting.json @@ -13,6 +13,7 @@ "my_account": "My Account", "config": "Configuration", "bot": "Bot & Webhook", + "notification_channels": "Canais de Notificação", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -23,7 +24,8 @@ "about": "About", "api_doc": "API Documentation", "version": "Version", - "video": "Video" + "video": "Video", + "notification": "Notificações" }, "vocespace": { "add": "Adicionar", @@ -285,5 +287,61 @@ "step_2": "Step Two: Input the login token", "step_2_desc": "You can get the token by access X-API-Key http header or just copy the text below:", "last": "Every API has a 'Try it Out' button, and you can debug the API with it" + }, + "notification": { + "desc": "Configurar preferências de notificação para diferentes canais", + "loading": "Carregando...", + "no_channels": "Nenhum canal de notificação disponível. Entre em contato com o administrador para configurar os canais de notificação.", + "status": "Status", + "enabled": "Ativado", + "not_configured": "Não configurado", + "configure": "Configurar", + "enable_notifications": "Ativar notificações", + "trigger_conditions": "Condições de acionamento", + "trigger_on_all_messages": "Receber todas as mensagens", + "trigger_on_mentions": "Notificar quando mencionado", + "skip_when_online": "Ignorar notificações quando online", + "select_groups": "Selecionar grupos", + "save": "Salvar", + "saving": "Salvando...", + "cancel": "Cancelar", + "delete": "Excluir configuração", + "deleting": "Excluindo...", + "save_success": "Preferências de notificação salvas com sucesso", + "save_error": "Falha ao salvar preferências de notificação", + "delete_success": "Preferências de notificação excluídas com sucesso", + "delete_error": "Falha ao excluir preferências de notificação" + }, + "admin_notification_channels": { + "desc": "Configurar canais de notificação para usuários", + "create_channel": "Criar Canal", + "edit_channel": "Editar", + "delete_channel": "Excluir", + "test_channel": "Testar", + "channel_name": "Nome do Canal", + "channel_type": "Tipo de Canal", + "channel_status": "Status", + "enabled": "Ativado", + "disabled": "Desativado", + "actions": "Ações", + "create_title": "Criar Canal de Notificação", + "create_desc": "Configurar um novo canal de notificação", + "edit_title": "Editar Canal de Notificação", + "edit_desc": "Atualizar configuração do canal", + "delete_title": "Excluir Canal", + "delete_desc": "Tem certeza de que deseja excluir este canal? Todas as preferências de usuário para este canal serão removidas.", + "test_success": "Notificação de teste enviada com sucesso", + "test_error": "Falha ao enviar notificação de teste", + "create_success": "Canal criado com sucesso", + "create_error": "Falha ao criar canal", + "update_success": "Canal atualizado com sucesso", + "update_error": "Falha ao atualizar canal", + "delete_success": "Canal excluído com sucesso", + "delete_error": "Falha ao excluir canal", + "config_section": "Configuração do Canal", + "basic_info": "Informações Básicas", + "enable_channel": "Ativar Canal", + "rate_limit": "Limite de taxa (solicitações por segundo)", + "no_channels": "Nenhum canal de notificação configurado ainda" } } diff --git a/public/locales/ru/setting.json b/public/locales/ru/setting.json index 0dc6b9fd..c86e0c9f 100644 --- a/public/locales/ru/setting.json +++ b/public/locales/ru/setting.json @@ -13,6 +13,7 @@ "my_account": "Мой аккаунт", "config": "Конфигурация", "bot": "Bot & Webhook", + "notification_channels": "Каналы уведомлений", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -23,7 +24,8 @@ "about": "About", "api_doc": "API Documentation", "version": "Версия", - "video": "Видео" + "video": "Видео", + "notification": "Уведомления" }, "vocespace": { "add": "Добавить", @@ -345,5 +347,61 @@ "step_2": "Шаг второй: введите токен входа", "step_2_desc": "Вы можете получить токен, обратившись к http-заголовку X-API-Key или просто скопировав текст ниже:", "last": "У каждого API есть кнопка «Попробовать», с помощью которой можно отлаживать API." + }, + "notification": { + "desc": "Настройка параметров уведомлений для различных каналов", + "loading": "Загрузка...", + "no_channels": "Нет доступных каналов уведомлений. Обратитесь к администратору для настройки каналов уведомлений.", + "status": "Статус", + "enabled": "Включено", + "not_configured": "Не настроено", + "configure": "Настроить", + "enable_notifications": "Включить уведомления", + "trigger_conditions": "Условия срабатывания", + "trigger_on_all_messages": "Получать все сообщения", + "trigger_on_mentions": "Уведомлять при упоминании", + "skip_when_online": "Пропускать уведомления в сети", + "select_groups": "Выбрать группы", + "save": "Сохранить", + "saving": "Сохранение...", + "cancel": "Отмена", + "delete": "Удалить конфигурацию", + "deleting": "Удаление...", + "save_success": "Настройки уведомлений успешно сохранены", + "save_error": "Не удалось сохранить настройки уведомлений", + "delete_success": "Настройки уведомлений успешно удалены", + "delete_error": "Не удалось удалить настройки уведомлений" + }, + "admin_notification_channels": { + "desc": "Настроить каналы уведомлений для пользователей", + "create_channel": "Создать канал", + "edit_channel": "Редактировать", + "delete_channel": "Удалить", + "test_channel": "Тест", + "channel_name": "Название канала", + "channel_type": "Тип канала", + "channel_status": "Статус", + "enabled": "Включено", + "disabled": "Отключено", + "actions": "Действия", + "create_title": "Создать канал уведомлений", + "create_desc": "Настроить новый канал уведомлений", + "edit_title": "Редактировать канал уведомлений", + "edit_desc": "Обновить конфигурацию канала", + "delete_title": "Удалить канал", + "delete_desc": "Вы уверены, что хотите удалить этот канал? Все пользовательские настройки для этого канала будут удалены.", + "test_success": "Тестовое уведомление успешно отправлено", + "test_error": "Не удалось отправить тестовое уведомление", + "create_success": "Канал успешно создан", + "create_error": "Не удалось создать канал", + "update_success": "Канал успешно обновлен", + "update_error": "Не удалось обновить канал", + "delete_success": "Канал успешно удален", + "delete_error": "Не удалось удалить канал", + "config_section": "Конфигурация канала", + "basic_info": "Основная информация", + "enable_channel": "Включить канал", + "rate_limit": "Ограничение скорости (запросов в секунду)", + "no_channels": "Каналы уведомлений еще не настроены" } } diff --git a/public/locales/tr/setting.json b/public/locales/tr/setting.json index c768ed3b..e3acfaa3 100644 --- a/public/locales/tr/setting.json +++ b/public/locales/tr/setting.json @@ -12,6 +12,7 @@ "my_account": "Hesabım", "config": "Yapılandırma", "bot": "Bot & Webhook", + "notification_channels": "Bildirim Kanalları", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -24,7 +25,8 @@ "version": "Sürüm", "faq": "SSS", "feedback": "Geri Bildirim", - "video": "Video" + "video": "Video", + "notification": "Bildirimler" }, "vocespace": { "add": "Ekle", @@ -238,5 +240,61 @@ "step_2": "İkinci Adım: Oturum açma jetonunu girin", "step_2_desc": "Jetonu 'X-API-Key' http header'e erişerek alabilir veya aşağıdaki metni kopyalayabilirsiniz:", "last": "Her API'de bir 'Deneyin' düğmesi vardır ve bununla API'de hata ayıklayabilirsiniz" + }, + "notification": { + "desc": "Farklı kanallar için bildirim tercihlerini yapılandırın", + "loading": "Yükleniyor...", + "no_channels": "Kullanılabilir bildirim kanalı yok. Bildirim kanallarını yapılandırmak için yöneticinizle iletişime geçin.", + "status": "Durum", + "enabled": "Etkin", + "not_configured": "Yapılandırılmamış", + "configure": "Yapılandır", + "enable_notifications": "Bildirimleri etkinleştir", + "trigger_conditions": "Tetikleme koşulları", + "trigger_on_all_messages": "Tüm mesajları al", + "trigger_on_mentions": "Bahsedildiğinde bildir", + "skip_when_online": "Çevrimiçiyken bildirimleri atla", + "select_groups": "Grupları seç", + "save": "Kaydet", + "saving": "Kaydediliyor...", + "cancel": "İptal", + "delete": "Yapılandırmayı sil", + "deleting": "Siliniyor...", + "save_success": "Bildirim tercihleri başarıyla kaydedildi", + "save_error": "Bildirim tercihleri kaydedilemedi", + "delete_success": "Bildirim tercihleri başarıyla silindi", + "delete_error": "Bildirim tercihleri silinemedi" + }, + "admin_notification_channels": { + "desc": "Kullanıcılar için bildirim kanallarını yapılandır", + "create_channel": "Kanal Oluştur", + "edit_channel": "Düzenle", + "delete_channel": "Sil", + "test_channel": "Test Et", + "channel_name": "Kanal Adı", + "channel_type": "Kanal Türü", + "channel_status": "Durum", + "enabled": "Etkin", + "disabled": "Devre Dışı", + "actions": "İşlemler", + "create_title": "Bildirim Kanalı Oluştur", + "create_desc": "Yeni bir bildirim kanalı yapılandır", + "edit_title": "Bildirim Kanalını Düzenle", + "edit_desc": "Kanal yapılandırmasını güncelle", + "delete_title": "Kanalı Sil", + "delete_desc": "Bu kanalı silmek istediğinizden emin misiniz? Bu kanal için tüm kullanıcı tercihleri kaldırılacaktır.", + "test_success": "Test bildirimi başarıyla gönderildi", + "test_error": "Test bildirimi gönderilemedi", + "create_success": "Kanal başarıyla oluşturuldu", + "create_error": "Kanal oluşturulamadı", + "update_success": "Kanal başarıyla güncellendi", + "update_error": "Kanal güncellenemedi", + "delete_success": "Kanal başarıyla silindi", + "delete_error": "Kanal silinemedi", + "config_section": "Kanal Yapılandırması", + "basic_info": "Temel Bilgiler", + "enable_channel": "Kanalı Etkinleştir", + "rate_limit": "Hız sınırı (saniye başına istek)", + "no_channels": "Henüz bildirim kanalı yapılandırılmamış" } } diff --git a/public/locales/zh/setting.json b/public/locales/zh/setting.json index 7a14fd52..f7907f34 100644 --- a/public/locales/zh/setting.json +++ b/public/locales/zh/setting.json @@ -12,6 +12,7 @@ "my_account": "我的账号", "config": "配置", "bot": "机器人&Webhook", + "notification_channels": "通知渠道", "firebase": "Firebase", "agora": "Agora", "smtp": "SMTP", @@ -23,7 +24,8 @@ "api_doc": "API 文档", "version": "版本信息", "feedback": "反馈", - "video": "音视频" + "video": "音视频", + "notification": "通知设置" }, "channel": { "leave": "离开", @@ -378,5 +380,61 @@ "step_2": "第二步:填入已登录的 Token", "step_2_desc": "Token 值本质是 JWT 中的 Token,所以有时长限制,如在使用时过期,请重新获取。获取方式可通过请求头 X-API-Key 拿到,或直接 Copy👇", "last": "最后:每个 API 都有 Try it Out 选项,可以由此调试该 API" + }, + "notification": { + "desc": "配置您的通知渠道", + "loading": "加载中...", + "no_channels": "暂无可用的通知渠道类型。请联系管理员。", + "status": "状态", + "enabled": "已启用", + "disabled": "已禁用", + "my_channels": "我的通知渠道", + "available_types": "可用渠道类型", + "create_channel": "创建渠道", + "edit_channel": "编辑渠道", + "channel_name": "渠道名称", + "channel_config": "渠道配置", + "enable_notifications": "启用通知", + "trigger_conditions": "触发条件", + "trigger_on_all_messages": "接收所有消息", + "trigger_on_mentions": "被提及时通知", + "skip_when_online": "在线时跳过通知", + "select_groups": "选择群组", + "save": "保存", + "saving": "保存中...", + "cancel": "取消", + "edit": "编辑", + "test": "测试", + "delete": "删除", + "save_success": "渠道保存成功", + "save_error": "渠道保存失败", + "create_success": "渠道创建成功", + "create_error": "渠道创建失败", + "delete_success": "渠道删除成功", + "delete_error": "渠道删除失败", + "test_success": "测试通知发送成功", + "test_error": "测试通知发送失败" + }, + "admin_notification_channels": { + "desc": "为用户启用通知渠道类型", + "enabled_types": "已启用的渠道类型", + "available_types": "可用渠道类型", + "enable_type": "启用", + "channel_type": "渠道类型", + "channel_status": "状态", + "enabled": "已启用", + "disabled": "已禁用", + "actions": "操作", + "delete_channel": "删除", + "delete_title": "删除渠道类型", + "delete_desc": "确定要删除此渠道类型吗?用户将无法再创建此类型的新渠道。", + "cancel": "取消", + "delete": "删除", + "create_success": "渠道类型启用成功", + "create_error": "渠道类型启用失败", + "update_success": "渠道类型更新成功", + "update_error": "渠道类型更新失败", + "delete_success": "渠道类型删除成功", + "delete_error": "渠道类型删除失败" } } diff --git a/src/app/services/notification.ts b/src/app/services/notification.ts new file mode 100644 index 00000000..ece8592e --- /dev/null +++ b/src/app/services/notification.ts @@ -0,0 +1,89 @@ +import { createApi } from "@reduxjs/toolkit/query/react"; +import baseQuery from "./base.query"; +import { + AvailableChannelType, + EnabledChannelType, + ToggleChannelTypeDTO, + UserNotificationChannel, + CreateUserChannelDTO, + UpdateUserChannelDTO, +} from "@/types/notification"; + +export const notificationApi = createApi({ + reducerPath: "notificationApi", + baseQuery, + tagTypes: ["EnabledChannelTypes", "AvailableChannelTypes", "UserChannels"], + endpoints: (builder) => ({ + // Admin endpoints + getEnabledChannelTypes: builder.query({ + query: () => ({ url: `/admin/notification/channel-types` }), + providesTags: ["EnabledChannelTypes"], + }), + toggleChannelType: builder.mutation({ + query: (data) => ({ + url: `/admin/notification/channel-types`, + method: "POST", + body: data, + }), + invalidatesTags: ["EnabledChannelTypes", "AvailableChannelTypes"], + }), + deleteChannelType: builder.mutation({ + query: (channelType) => ({ + url: `/admin/notification/channel-types/${channelType}`, + method: "DELETE", + }), + invalidatesTags: ["EnabledChannelTypes", "AvailableChannelTypes"], + }), + + // User endpoints + getAvailableChannelTypes: builder.query({ + query: () => ({ url: `/user/notification/available-types` }), + providesTags: ["AvailableChannelTypes"], + }), + getUserChannels: builder.query({ + query: () => ({ url: `/user/notification/channels` }), + providesTags: ["UserChannels"], + }), + createUserChannel: builder.mutation({ + query: (data) => ({ + url: `/user/notification/channels`, + method: "POST", + body: data, + }), + invalidatesTags: ["UserChannels"], + }), + updateUserChannel: builder.mutation({ + query: ({ id, ...data }) => ({ + url: `/user/notification/channels/${id}`, + method: "PUT", + body: data, + }), + invalidatesTags: ["UserChannels"], + }), + deleteUserChannel: builder.mutation({ + query: (id) => ({ + url: `/user/notification/channels/${id}`, + method: "DELETE", + }), + invalidatesTags: ["UserChannels"], + }), + testUserChannel: builder.mutation({ + query: (id) => ({ + url: `/user/notification/channels/${id}/test`, + method: "POST", + }), + }), + }), +}); + +export const { + useGetEnabledChannelTypesQuery, + useToggleChannelTypeMutation, + useDeleteChannelTypeMutation, + useGetAvailableChannelTypesQuery, + useGetUserChannelsQuery, + useCreateUserChannelMutation, + useUpdateUserChannelMutation, + useDeleteUserChannelMutation, + useTestUserChannelMutation, +} = notificationApi; diff --git a/src/app/store.ts b/src/app/store.ts index 138f3a39..d7c5ce61 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -6,6 +6,7 @@ import listenerMiddleware from "./listener.middleware"; import { authApi } from "./services/auth"; import { channelApi } from "./services/channel"; import { messageApi } from "./services/message"; +import { notificationApi } from "./services/notification"; import { serverApi } from "./services/server"; import { userApi } from "./services/user"; import authDataReducer from "./slices/auth.data"; @@ -44,7 +45,8 @@ const reducer = combineReducers({ [messageApi.reducerPath]: messageApi.reducer, [userApi.reducerPath]: userApi.reducer, [channelApi.reducerPath]: channelApi.reducer, - [serverApi.reducerPath]: serverApi.reducer + [serverApi.reducerPath]: serverApi.reducer, + [notificationApi.reducerPath]: notificationApi.reducer }); const store = configureStore({ @@ -56,7 +58,8 @@ const store = configureStore({ userApi.middleware, channelApi.middleware, serverApi.middleware, - messageApi.middleware + messageApi.middleware, + notificationApi.middleware ) .prepend(listenerMiddleware.middleware) }); diff --git a/src/routes/setting/AdminNotificationChannels/channelSchemas.ts b/src/routes/setting/AdminNotificationChannels/channelSchemas.ts new file mode 100644 index 00000000..b27790fd --- /dev/null +++ b/src/routes/setting/AdminNotificationChannels/channelSchemas.ts @@ -0,0 +1,310 @@ +import { ChannelSchema, ChannelType } from "@/types/notification"; + +export const channelSchemas: ChannelSchema[] = [ + { + type: "telegram", + name: "Telegram Bot", + description: "Telegram Bot API with media support", + fields: [ + { + name: "bot_token", + type: "text", + label: "Bot Token", + placeholder: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", + required: true, + description: "Get from @BotFather", + }, + { + name: "chat_id", + type: "text", + label: "Chat ID", + placeholder: "@your_channel or user_id", + required: true, + description: "Channel username or user ID", + }, + { + name: "parse_mode", + type: "select", + label: "Parse Mode", + required: false, + description: "Message formatting mode", + options: [ + { value: "", label: "None" }, + { value: "Markdown", label: "Markdown" }, + { value: "MarkdownV2", label: "MarkdownV2" }, + { value: "HTML", label: "HTML" }, + ], + }, + ], + }, + { + type: "discord", + name: "Discord Webhook", + description: "Discord webhook notifications with rich embeds", + fields: [ + { + name: "webhook_url", + type: "url", + label: "Webhook URL", + placeholder: "https://discord.com/api/webhooks/...", + required: true, + description: "Discord webhook URL", + }, + { + name: "username", + type: "text", + label: "Username", + placeholder: "VoceChat Bot", + required: false, + description: "Override webhook username", + }, + { + name: "avatar_url", + type: "url", + label: "Avatar URL", + placeholder: "https://example.com/avatar.png", + required: false, + description: "Override webhook avatar", + }, + ], + }, + { + type: "feishu", + name: "Feishu Bot", + description: "Feishu (Lark) webhook notifications", + fields: [ + { + name: "webhook_url", + type: "url", + label: "Webhook URL", + placeholder: "https://open.feishu.cn/open-apis/bot/v2/hook/...", + required: true, + description: "Feishu webhook URL", + }, + { + name: "sign_secret", + type: "text", + label: "Sign Secret", + placeholder: "Optional signing secret", + required: false, + description: "For signature verification", + }, + ], + }, + { + type: "dingtalk", + name: "DingTalk Bot", + description: "DingTalk webhook notifications", + fields: [ + { + name: "webhook_url", + type: "url", + label: "Webhook URL", + placeholder: "https://oapi.dingtalk.com/robot/send?access_token=...", + required: true, + description: "DingTalk webhook URL", + }, + { + name: "sign_secret", + type: "text", + label: "Sign Secret", + placeholder: "Optional signing secret", + required: false, + description: "For signature verification", + }, + ], + }, + { + type: "wecombot", + name: "WeCom Bot", + description: "WeCom (WeChat Work) webhook notifications", + fields: [ + { + name: "webhook_url", + type: "url", + label: "Webhook URL", + placeholder: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + required: true, + description: "WeCom webhook URL", + }, + ], + }, + { + type: "bark", + name: "Bark", + description: "Bark iOS notification service", + fields: [ + { + name: "server_url", + type: "url", + label: "Server URL", + placeholder: "https://api.day.app", + required: true, + description: "Bark server URL", + }, + { + name: "device_key", + type: "text", + label: "Device Key", + placeholder: "Your device key", + required: true, + description: "Device key from Bark app", + }, + { + name: "sound", + type: "text", + label: "Sound", + placeholder: "default", + required: false, + description: "Notification sound name", + }, + ], + }, + { + type: "gotify", + name: "Gotify", + description: "Gotify self-hosted notification server", + fields: [ + { + name: "server_url", + type: "url", + label: "Server URL", + placeholder: "https://gotify.example.com", + required: true, + description: "Gotify server URL", + }, + { + name: "app_token", + type: "text", + label: "App Token", + placeholder: "Application token", + required: true, + description: "Application token from Gotify", + }, + { + name: "priority", + type: "number", + label: "Priority", + placeholder: "5", + required: false, + description: "Message priority (0-10)", + }, + ], + }, + { + type: "serverchan", + name: "Server Chan", + description: "Server Chan notification service", + fields: [ + { + name: "send_key", + type: "text", + label: "SendKey", + placeholder: "Your SendKey", + required: true, + description: "SendKey from Server Chan", + }, + ], + }, + { + type: "pushdeer", + name: "PushDeer", + description: "PushDeer notification service", + fields: [ + { + name: "server_url", + type: "url", + label: "Server URL", + placeholder: "https://api2.pushdeer.com", + required: true, + description: "PushDeer server URL", + }, + { + name: "push_key", + type: "text", + label: "Push Key", + placeholder: "Your push key", + required: true, + description: "Push key from PushDeer", + }, + ], + }, + { + type: "matrix", + name: "Matrix", + description: "Matrix protocol notifications", + fields: [ + { + name: "homeserver_url", + type: "url", + label: "Homeserver URL", + placeholder: "https://matrix.org", + required: true, + description: "Matrix homeserver URL", + }, + { + name: "access_token", + type: "text", + label: "Access Token", + placeholder: "Your access token", + required: true, + description: "Matrix access token", + }, + { + name: "room_id", + type: "text", + label: "Room ID", + placeholder: "!roomid:matrix.org", + required: true, + description: "Matrix room ID", + }, + ], + }, + { + type: "custom_json", + name: "Custom JSON Webhook", + description: "Custom HTTP webhook with JSON payload", + fields: [ + { + name: "webhook_url", + type: "url", + label: "Webhook URL", + placeholder: "https://example.com/webhook", + required: true, + description: "HTTP endpoint URL", + }, + { + name: "method", + type: "select", + label: "HTTP Method", + required: true, + description: "HTTP request method", + options: [ + { value: "POST", label: "POST" }, + { value: "PUT", label: "PUT" }, + { value: "PATCH", label: "PATCH" }, + ], + }, + { + name: "headers", + type: "textarea", + label: "Custom Headers", + placeholder: '{"Authorization": "Bearer token"}', + required: false, + description: "JSON object with custom headers", + }, + { + name: "body_template", + type: "textarea", + label: "Body Template", + placeholder: '{"message": "{{content}}", "user": "{{sender}}"}', + required: false, + description: "JSON template with placeholders", + }, + ], + }, +]; + +export function getChannelSchema(type: ChannelType): ChannelSchema | undefined { + return channelSchemas.find((s) => s.type === type); +} diff --git a/src/routes/setting/AdminNotificationChannels/index.tsx b/src/routes/setting/AdminNotificationChannels/index.tsx new file mode 100644 index 00000000..879fdeed --- /dev/null +++ b/src/routes/setting/AdminNotificationChannels/index.tsx @@ -0,0 +1,224 @@ +import { useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + +import { + useGetEnabledChannelTypesQuery, + useToggleChannelTypeMutation, + useDeleteChannelTypeMutation, +} from "@/app/services/notification"; +import { ConfigTip } from "@/components/ConfigTip"; +import Button from "@/components/styled/Button"; +import Toggle from "@/components/styled/Toggle"; +import { EnabledChannelType, ChannelType } from "@/types/notification"; +import { channelSchemas } from "./channelSchemas"; +import ServerVersionChecker from "@/components/ServerVersionChecker"; + +export default function AdminNotificationChannels() { + const { t } = useTranslation("setting", { keyPrefix: "admin_notification_channels" }); + const { data: channelTypes = [], isLoading } = useGetEnabledChannelTypesQuery(); + const [toggleChannelType] = useToggleChannelTypeMutation(); + const [deleteChannelType] = useDeleteChannelTypeMutation(); + + const [deletingType, setDeletingType] = useState(null); + + const handleToggle = async (channelType: ChannelType, currentEnabled: boolean) => { + try { + await toggleChannelType({ + channel_type: channelType, + enabled: !currentEnabled, + }).unwrap(); + toast.success(t("update_success")); + } catch (error) { + toast.error(t("update_error")); + console.error("Failed to toggle channel type:", error); + } + }; + + const handleDelete = async (channelType: string) => { + try { + await deleteChannelType(channelType).unwrap(); + toast.success(t("delete_success")); + setDeletingType(null); + } catch (error) { + toast.error(t("delete_error")); + console.error("Failed to delete channel type:", error); + } + }; + + const handleEnableNew = async (channelType: ChannelType) => { + try { + await toggleChannelType({ + channel_type: channelType, + enabled: true, + }).unwrap(); + toast.success(t("create_success")); + } catch (error) { + toast.error(t("create_error")); + console.error("Failed to enable channel type:", error); + } + }; + + // Get all available channel types + const allChannelTypes = channelSchemas.map((s) => s.type); + const enabledTypes = new Set(channelTypes.map((ct) => ct.channel_type)); + const availableToEnable = allChannelTypes.filter((type) => !enabledTypes.has(type)); + + // Debug logging + console.log("Admin Notification Channels Debug:", { + allChannelTypes, + enabledTypes: Array.from(enabledTypes), + availableToEnable, + channelTypesFromAPI: channelTypes, + }); + + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } + + return ( + +
+ + + {/* Enabled Channel Types */} + {channelTypes.length > 0 && ( +
+

+ {t("enabled_types")} +

+
+ + + + + + + + + + {channelTypes.map((channelType) => { + const schema = channelSchemas.find((s) => s.type === channelType.channel_type); + return ( + + + + + + ); + })} + +
+ {t("channel_type")} + + {t("channel_status")} + + {t("actions")} +
+
+
+ {schema?.name || channelType.channel_type} +
+
+ {schema?.description} +
+
+
+
+ + handleToggle(channelType.channel_type, channelType.enabled) + } + /> + + {channelType.enabled ? t("enabled") : t("disabled")} + +
+
+
+ +
+
+
+
+ )} + + {/* Available Channel Types to Enable */} +
+

+ {t("available_types")} +

+ {availableToEnable.length > 0 ? ( +
+ {availableToEnable.map((type) => { + const schema = channelSchemas.find((s) => s.type === type); + if (!schema) return null; + return ( +
+

+ {schema.name} +

+

+ {schema.description} +

+ +
+ ); + })} +
+ ) : ( +

+ All channel types are already enabled. Total schemas: {channelSchemas.length} +

+ )} +
+ + {/* Delete Confirmation Modal */} + {deletingType && ( +
+
+

+ {t("delete_title")} +

+

+ {t("delete_desc")} +

+
+ + +
+
+
+ )} +
+
+ ); +} diff --git a/src/routes/setting/NotificationSettings.tsx b/src/routes/setting/NotificationSettings.tsx new file mode 100644 index 00000000..6b520586 --- /dev/null +++ b/src/routes/setting/NotificationSettings.tsx @@ -0,0 +1,430 @@ +import { useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { shallowEqual } from "react-redux"; + +import { + useGetAvailableChannelTypesQuery, + useGetUserChannelsQuery, + useCreateUserChannelMutation, + useUpdateUserChannelMutation, + useDeleteUserChannelMutation, + useTestUserChannelMutation, +} from "@/app/services/notification"; +import { useAppSelector } from "@/app/store"; +import { ConfigTip } from "@/components/ConfigTip"; +import Button from "@/components/styled/Button"; +import Checkbox from "@/components/styled/Checkbox"; +import Input from "@/components/styled/Input"; +import Textarea from "@/components/styled/Textarea"; +import Select from "@/components/styled/Select"; +import Label from "@/components/styled/Label"; +import Toggle from "@/components/styled/Toggle"; +import { UserNotificationChannel, ChannelType, CreateUserChannelDTO } from "@/types/notification"; +import { getChannelSchema } from "./AdminNotificationChannels/channelSchemas"; +import ServerVersionChecker from "@/components/ServerVersionChecker"; + +export default function NotificationSettings() { + const { t } = useTranslation("setting", { keyPrefix: "notification" }); + const [editingChannelId, setEditingChannelId] = useState(null); + const [creatingType, setCreatingType] = useState(null); + const [formData, setFormData] = useState>({}); + + // Fetch data + const { data: availableTypes = [], isLoading: typesLoading } = useGetAvailableChannelTypesQuery(); + const { data: userChannels = [], isLoading: channelsLoading } = useGetUserChannelsQuery(); + const channels = useAppSelector((store) => Object.values(store.channels.byId), shallowEqual); + + // Mutations + const [createChannel, { isLoading: isCreating }] = useCreateUserChannelMutation(); + const [updateChannel, { isLoading: isUpdating }] = useUpdateUserChannelMutation(); + const [deleteChannel, { isLoading: isDeleting }] = useDeleteUserChannelMutation(); + const [testChannel] = useTestUserChannelMutation(); + + const handleCreateClick = (channelType: ChannelType) => { + setCreatingType(channelType); + setFormData({ + channel_type: channelType, + name: "", + enabled: true, + config: {}, + trigger_on_all_messages: false, + trigger_on_mentions: true, + trigger_on_groups: [], + skip_when_online: true, + }); + }; + + const handleEditClick = (channel: UserNotificationChannel) => { + setEditingChannelId(channel.id); + setFormData({ + channel_type: channel.channel_type, + name: channel.name, + enabled: channel.enabled, + config: channel.config, + trigger_on_all_messages: channel.trigger_on_all_messages, + trigger_on_mentions: channel.trigger_on_mentions, + trigger_on_groups: channel.trigger_on_groups, + skip_when_online: channel.skip_when_online, + rate_limit_rps: channel.rate_limit_rps, + }); + }; + + const handleCancel = () => { + setEditingChannelId(null); + setCreatingType(null); + setFormData({}); + }; + + const handleSave = async () => { + if (!formData.name?.trim()) { + toast.error("Channel name is required"); + return; + } + + const schema = getChannelSchema(formData.channel_type!); + if (schema) { + const missingFields = schema.fields + .filter((field) => field.required && !formData.config?.[field.name]) + .map((field) => field.label); + + if (missingFields.length > 0) { + toast.error(`Missing required fields: ${missingFields.join(", ")}`); + return; + } + } + + try { + if (editingChannelId) { + await updateChannel({ + id: editingChannelId, + ...(formData as CreateUserChannelDTO), + }).unwrap(); + toast.success(t("save_success")); + } else { + await createChannel(formData as CreateUserChannelDTO).unwrap(); + toast.success(t("create_success")); + } + handleCancel(); + } catch (error) { + toast.error(editingChannelId ? t("save_error") : t("create_error")); + console.error("Failed to save channel:", error); + } + }; + + const handleDelete = async (channelId: number) => { + try { + await deleteChannel(channelId).unwrap(); + toast.success(t("delete_success")); + } catch (error) { + toast.error(t("delete_error")); + console.error("Failed to delete channel:", error); + } + }; + + const handleTest = async (channelId: number) => { + try { + await testChannel(channelId).unwrap(); + toast.success(t("test_success")); + } catch (error) { + toast.error(t("test_error")); + console.error("Failed to test channel:", error); + } + }; + + const handleConfigChange = (fieldName: string, value: any) => { + setFormData((prev) => ({ + ...prev, + config: { + ...prev.config, + [fieldName]: value, + }, + })); + }; + + const handleGroupToggle = (groupId: number) => { + setFormData((prev) => { + const groups = prev.trigger_on_groups || []; + const newGroups = groups.includes(groupId) + ? groups.filter((id) => id !== groupId) + : [...groups, groupId]; + return { ...prev, trigger_on_groups: newGroups }; + }); + }; + + if (typesLoading || channelsLoading) { + return ( +
+
{t("loading")}
+
+ ); + } + + if (availableTypes.length === 0) { + return ( +
+ +
{t("no_channels")}
+
+ ); + } + + const schema = formData.channel_type ? getChannelSchema(formData.channel_type) : null; + const isEditing = editingChannelId !== null || creatingType !== null; + + return ( + +
+ + + {/* User's Configured Channels */} + {userChannels.length > 0 && ( +
+

+ {t("my_channels")} +

+
+ {userChannels.map((channel) => { + const channelSchema = getChannelSchema(channel.channel_type); + return ( +
+
+
+

+ {channel.name} +

+

+ {channelSchema?.name || channel.channel_type} +

+

+ {t("status")}:{" "} + + {channel.enabled ? t("enabled") : t("disabled")} + +

+
+
+ + + +
+
+
+ ); + })} +
+
+ )} + + {/* Available Channel Types */} + {!isEditing && ( +
+

+ {t("available_types")} +

+
+ {availableTypes.map((type) => { + const schema = getChannelSchema(type.id as ChannelType); + return ( +
+

+ {type.name} +

+

+ {type.description} +

+ +
+ ); + })} +
+
+ )} + + {/* Create/Edit Form */} + {isEditing && formData.channel_type && ( +
+

+ {editingChannelId ? t("edit_channel") : t("create_channel")} +

+ +
+ {/* Channel Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="My Notification Channel" + className="w-full" + /> +
+ + {/* Channel Configuration */} + {schema && schema.fields.length > 0 && ( +
+ +

+ {schema.description} +

+ + {schema.fields.map((field) => ( +
+ + + {field.type === "textarea" ? ( +