feat: 增加消息推送设置 (#302)

* feat: 增加消息推送设置

* chore: bump version to v0.9.52
This commit is contained in:
haorwen
2026-01-18 22:52:39 +08:00
committed by GitHub
parent 87f0992ae5
commit 1b1645846f
18 changed files with 1710 additions and 24 deletions
+1 -1
View File
@@ -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",
+6 -11
View File
@@ -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:
+60 -2
View File
@@ -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"
}
}
+59 -1
View File
@@ -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"
}
}
+59 -1
View File
@@ -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"
}
}
+59 -1
View File
@@ -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"
}
}
+60 -1
View File
@@ -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": "通知チャンネルがまだ設定されていません"
}
}
+59 -1
View File
@@ -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"
}
}
+59 -1
View File
@@ -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": "Каналы уведомлений еще не настроены"
}
}
+59 -1
View File
@@ -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ış"
}
}
+59 -1
View File
@@ -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": "渠道类型删除失败"
}
}
+89
View File
@@ -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<EnabledChannelType[], void>({
query: () => ({ url: `/admin/notification/channel-types` }),
providesTags: ["EnabledChannelTypes"],
}),
toggleChannelType: builder.mutation<EnabledChannelType, ToggleChannelTypeDTO>({
query: (data) => ({
url: `/admin/notification/channel-types`,
method: "POST",
body: data,
}),
invalidatesTags: ["EnabledChannelTypes", "AvailableChannelTypes"],
}),
deleteChannelType: builder.mutation<string, string>({
query: (channelType) => ({
url: `/admin/notification/channel-types/${channelType}`,
method: "DELETE",
}),
invalidatesTags: ["EnabledChannelTypes", "AvailableChannelTypes"],
}),
// User endpoints
getAvailableChannelTypes: builder.query<AvailableChannelType[], void>({
query: () => ({ url: `/user/notification/available-types` }),
providesTags: ["AvailableChannelTypes"],
}),
getUserChannels: builder.query<UserNotificationChannel[], void>({
query: () => ({ url: `/user/notification/channels` }),
providesTags: ["UserChannels"],
}),
createUserChannel: builder.mutation<UserNotificationChannel, CreateUserChannelDTO>({
query: (data) => ({
url: `/user/notification/channels`,
method: "POST",
body: data,
}),
invalidatesTags: ["UserChannels"],
}),
updateUserChannel: builder.mutation<UserNotificationChannel, UpdateUserChannelDTO>({
query: ({ id, ...data }) => ({
url: `/user/notification/channels/${id}`,
method: "PUT",
body: data,
}),
invalidatesTags: ["UserChannels"],
}),
deleteUserChannel: builder.mutation<string, number>({
query: (id) => ({
url: `/user/notification/channels/${id}`,
method: "DELETE",
}),
invalidatesTags: ["UserChannels"],
}),
testUserChannel: builder.mutation<string, number>({
query: (id) => ({
url: `/user/notification/channels/${id}/test`,
method: "POST",
}),
}),
}),
});
export const {
useGetEnabledChannelTypesQuery,
useToggleChannelTypeMutation,
useDeleteChannelTypeMutation,
useGetAvailableChannelTypesQuery,
useGetUserChannelsQuery,
useCreateUserChannelMutation,
useUpdateUserChannelMutation,
useDeleteUserChannelMutation,
useTestUserChannelMutation,
} = notificationApi;
+5 -2
View File
@@ -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)
});
@@ -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);
}
@@ -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<string | null>(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 (
<div className="setting-container max-md:w-full">
<div className="text-gray-500">Loading...</div>
</div>
);
}
return (
<ServerVersionChecker version="0.5.11">
<div className="setting-container max-md:w-full">
<ConfigTip title={t("desc")} />
{/* Enabled Channel Types */}
{channelTypes.length > 0 && (
<div className="mt-6">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("enabled_types")}
</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 font-semibold text-gray-700 dark:text-gray-300">
{t("channel_type")}
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 dark:text-gray-300">
{t("channel_status")}
</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700 dark:text-gray-300">
{t("actions")}
</th>
</tr>
</thead>
<tbody>
{channelTypes.map((channelType) => {
const schema = channelSchemas.find((s) => s.type === channelType.channel_type);
return (
<tr
key={channelType.id}
className="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800"
>
<td className="py-3 px-4">
<div>
<div className="font-medium text-gray-900 dark:text-gray-100">
{schema?.name || channelType.channel_type}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{schema?.description}
</div>
</div>
</td>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Toggle
checked={channelType.enabled}
onClick={() =>
handleToggle(channelType.channel_type, channelType.enabled)
}
/>
<span
className={
channelType.enabled
? "text-green-600 dark:text-green-400"
: "text-gray-500"
}
>
{channelType.enabled ? t("enabled") : t("disabled")}
</span>
</div>
</td>
<td className="py-3 px-4">
<div className="flex justify-end gap-2">
<Button
className="mini danger"
onClick={() => setDeletingType(channelType.channel_type)}
>
{t("delete_channel")}
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Available Channel Types to Enable */}
<div className="mt-8">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("available_types")}
</h3>
{availableToEnable.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{availableToEnable.map((type) => {
const schema = channelSchemas.find((s) => s.type === type);
if (!schema) return null;
return (
<div
key={type}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800 flex flex-col"
>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{schema.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 flex-1">
{schema.description}
</p>
<Button onClick={() => handleEnableNew(type)} className="w-full">
{t("enable_type")}
</Button>
</div>
);
})}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400">
All channel types are already enabled. Total schemas: {channelSchemas.length}
</p>
)}
</div>
{/* Delete Confirmation Modal */}
{deletingType && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-900 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("delete_title")}
</h3>
<p className="text-gray-700 dark:text-gray-300 mb-6">
{t("delete_desc")}
</p>
<div className="flex gap-3 justify-end">
<Button onClick={() => setDeletingType(null)} className="cancel">
{t("cancel")}
</Button>
<Button onClick={() => handleDelete(deletingType)} className="danger">
{t("delete")}
</Button>
</div>
</div>
</div>
)}
</div>
</ServerVersionChecker>
);
}
+430
View File
@@ -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<number | null>(null);
const [creatingType, setCreatingType] = useState<ChannelType | null>(null);
const [formData, setFormData] = useState<Partial<CreateUserChannelDTO>>({});
// 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 (
<div className="setting-container max-md:w-full">
<div className="text-gray-500">{t("loading")}</div>
</div>
);
}
if (availableTypes.length === 0) {
return (
<div className="setting-container max-md:w-full">
<ConfigTip title={t("desc")} />
<div className="text-gray-500 mt-4">{t("no_channels")}</div>
</div>
);
}
const schema = formData.channel_type ? getChannelSchema(formData.channel_type) : null;
const isEditing = editingChannelId !== null || creatingType !== null;
return (
<ServerVersionChecker version="0.5.11">
<div className="setting-container max-md:w-full">
<ConfigTip title={t("desc")} />
{/* User's Configured Channels */}
{userChannels.length > 0 && (
<div className="mt-6">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("my_channels")}
</h3>
<div className="space-y-3">
{userChannels.map((channel) => {
const channelSchema = getChannelSchema(channel.channel_type);
return (
<div
key={channel.id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{channel.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{channelSchema?.name || channel.channel_type}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
{t("status")}:{" "}
<span
className={
channel.enabled
? "text-green-600 dark:text-green-400"
: "text-gray-500"
}
>
{channel.enabled ? t("enabled") : t("disabled")}
</span>
</p>
</div>
<div className="flex gap-2 ml-4">
<Button className="mini" onClick={() => handleEditClick(channel)}>
{t("edit")}
</Button>
<Button className="mini" onClick={() => handleTest(channel.id)}>
{t("test")}
</Button>
<Button
className="mini danger"
onClick={() => handleDelete(channel.id)}
disabled={isDeleting}
>
{t("delete")}
</Button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Available Channel Types */}
{!isEditing && (
<div className="mt-8">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("available_types")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{availableTypes.map((type) => {
const schema = getChannelSchema(type.id as ChannelType);
return (
<div
key={type.id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800 flex flex-col"
>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{type.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 flex-1">
{type.description}
</p>
<Button
onClick={() => handleCreateClick(type.id as ChannelType)}
className="w-full"
>
{t("create_channel")}
</Button>
</div>
);
})}
</div>
</div>
)}
{/* Create/Edit Form */}
{isEditing && formData.channel_type && (
<div className="mt-8 border border-gray-200 dark:border-gray-700 rounded-lg p-6 bg-white dark:bg-gray-800">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-6">
{editingChannelId ? t("edit_channel") : t("create_channel")}
</h3>
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
{/* Channel Name */}
<div>
<Label className="block mb-2">{t("channel_name")} *</Label>
<Input
value={formData.name || ""}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="My Notification Channel"
className="w-full"
/>
</div>
{/* Channel Configuration */}
{schema && schema.fields.length > 0 && (
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<Label className="block font-semibold mb-3">{t("channel_config")}</Label>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
{schema.description}
</p>
{schema.fields.map((field) => (
<div key={field.name} className="mb-3">
<Label className="block mb-2">
{field.label} {field.required && "*"}
</Label>
{field.type === "textarea" ? (
<Textarea
value={formData.config?.[field.name] || ""}
onChange={(e) => handleConfigChange(field.name, e.target.value)}
placeholder={field.placeholder}
className="w-full"
rows={3}
/>
) : field.type === "select" ? (
<Select
value={formData.config?.[field.name] || ""}
onChange={(e) => handleConfigChange(field.name, e.target.value)}
className="w-full"
>
{field.options?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Select>
) : (
<Input
type={field.type}
value={formData.config?.[field.name] || ""}
onChange={(e) => handleConfigChange(field.name, e.target.value)}
placeholder={field.placeholder}
className="w-full"
/>
)}
{field.description && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{field.description}
</p>
)}
</div>
))}
</div>
)}
{/* Enable Channel */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<Label>{t("enable_notifications")}</Label>
<Toggle
checked={formData.enabled || false}
onClick={() => setFormData({ ...formData, enabled: !formData.enabled })}
/>
</div>
{/* Trigger Conditions */}
<div className="space-y-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<Label className="block font-semibold">{t("trigger_conditions")}</Label>
<div className="flex items-center gap-2">
<Checkbox
checked={formData.trigger_on_all_messages || false}
onClick={() =>
setFormData({
...formData,
trigger_on_all_messages: !formData.trigger_on_all_messages,
})
}
/>
<span className="text-sm">{t("trigger_on_all_messages")}</span>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={formData.trigger_on_mentions || false}
onClick={() =>
setFormData({
...formData,
trigger_on_mentions: !formData.trigger_on_mentions,
})
}
/>
<span className="text-sm">{t("trigger_on_mentions")}</span>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={formData.skip_when_online || false}
onClick={() =>
setFormData({
...formData,
skip_when_online: !formData.skip_when_online,
})
}
/>
<span className="text-sm">{t("skip_when_online")}</span>
</div>
</div>
{/* Select Groups */}
{channels.length > 0 && (
<div className="space-y-2 pt-4 border-t border-gray-200 dark:border-gray-700">
<Label className="block font-semibold">{t("select_groups")}</Label>
<div className="space-y-2 max-h-48 overflow-y-auto">
{channels.map((group) => (
<div key={group.gid} className="flex items-center gap-2">
<Checkbox
checked={(formData.trigger_on_groups || []).includes(group.gid)}
onClick={() => handleGroupToggle(group.gid)}
/>
<span className="text-sm">{group.name}</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Action Buttons */}
<div className="flex gap-2 pt-6 mt-6 border-t border-gray-200 dark:border-gray-700">
<Button onClick={handleSave} disabled={isCreating || isUpdating}>
{isCreating || isUpdating ? t("saving") : t("save")}
</Button>
<Button onClick={handleCancel} disabled={isCreating || isUpdating}>
{t("cancel")}
</Button>
</div>
</div>
)}
</div>
</ServerVersionChecker>
);
}
+11
View File
@@ -13,10 +13,12 @@ import ConfigSMTP from "./config/SMTP";
import DataManagement from "./DataManagement";
import License from "./License";
import MyAccount from "./MyAccount";
import NotificationSettings from "./NotificationSettings";
import Overview from "./Overview";
import Widget from "./Widget";
import { shallowEqual } from "react-redux";
import { ConfigVocespace } from "./config/Vocespace";
import AdminNotificationChannels from "./AdminNotificationChannels";
const dataManagementNav = {
name: "data_management",
@@ -35,6 +37,10 @@ const navs = [
name: "my_account",
component: <MyAccount />,
},
{
name: "notification",
component: <NotificationSettings />,
},
{
name: "members",
component: <ManageMembers />,
@@ -50,6 +56,11 @@ const navs = [
component: <BotConfig />,
admin: true,
},
{
name: "notification_channels",
component: <AdminNotificationChannels />,
admin: true,
},
{
name: "firebase",
component: <ConfigFirebase />,
+101
View File
@@ -0,0 +1,101 @@
// Notification channel types
/**
* Channel types supported by the notification system
*/
export type ChannelType =
| 'telegram' | 'discord' | 'feishu' | 'dingtalk'
| 'wecombot' | 'bark' | 'gotify' | 'serverchan'
| 'pushdeer' | 'matrix' | 'custom_json';
/**
* Available channel type (returned by user API)
*/
export interface AvailableChannelType {
id: string; // channel_type as string
name: string;
description: string;
}
/**
* Admin: Enabled channel type configuration
*/
export interface EnabledChannelType {
id: number;
channel_type: ChannelType;
enabled: boolean;
created_at: number;
updated_at: number;
}
/**
* Admin: DTO for enabling/disabling channel type
*/
export interface ToggleChannelTypeDTO {
channel_type: ChannelType;
enabled: boolean;
}
/**
* User: Notification channel configuration
*/
export interface UserNotificationChannel {
id: number;
user_id: number;
channel_type: ChannelType;
name: string;
enabled: boolean;
config: Record<string, any>;
trigger_on_all_messages: boolean;
trigger_on_mentions: boolean;
trigger_on_groups: number[];
skip_when_online: boolean;
rate_limit_rps?: number;
created_at: number;
updated_at: number;
}
/**
* User: DTO for creating user notification channel
*/
export interface CreateUserChannelDTO {
channel_type: ChannelType;
name: string;
enabled: boolean;
config: Record<string, any>;
trigger_on_all_messages: boolean;
trigger_on_mentions: boolean;
trigger_on_groups: number[];
skip_when_online: boolean;
rate_limit_rps?: number;
}
/**
* User: DTO for updating user notification channel
*/
export interface UpdateUserChannelDTO extends CreateUserChannelDTO {
id: number;
}
/**
* Schema definition for channel configuration fields
*/
export interface ChannelFieldSchema {
name: string;
type: 'text' | 'url' | 'number' | 'textarea' | 'select';
label: string;
placeholder?: string;
required: boolean;
description?: string;
options?: { value: string; label: string }[];
}
/**
* Schema definition for a channel type
*/
export interface ChannelSchema {
type: ChannelType;
name: string;
description: string;
fields: ChannelFieldSchema[];
}