diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index f7cdac7..b1d9f14 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -29,7 +29,8 @@ "copiedToClipboard": "In die Zwischenablage kopiert", "copyToClipboard": "In die Zwischenablage kopieren", "show": "Anzeigen", - "hide": "Verbergen" + "hide": "Verbergen", + "reload": "Neu laden" } }, "sideMenu": { @@ -386,7 +387,6 @@ "repeatNewPassword": "Neues Passwort wiederholen", "language": "Sprache" }, - "button": { "createApiKey": { "title": "Neuen API-Schlüssel erstellen", @@ -396,7 +396,14 @@ } } }, - "icon": { "viewApiDoc": "Api-Dokumentation anschauen" } + "icon": { "viewApiDoc": "Api-Dokumentation anschauen" }, + "telegram": { + "title": "Telegram Benachrichtigungen", + "disconnectPopconfirm": { + "title": "Sind Sie sicher, dass Sie die Verbindung zu Telegram trennen wollen?", + "description": "Sie werden keine Benachrichtigungen mehr erhalten" + } + } }, "scanners": { "column": { diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index f740c95..53124f4 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -29,7 +29,8 @@ "copiedToClipboard": "Copied to clipboard", "copyToClipboard": "Copy to clipboard", "show": "Show", - "hide": "Hide" + "hide": "Hide", + "reload": "Reload" } }, "sideMenu": { @@ -415,7 +416,21 @@ } } }, - "icon": { "viewApiDoc": "View API Documentation" } + "icon": { "viewApiDoc": "View API Documentation" }, + "telegram": { + "title": "Telegram notifications", + "subscribeButton": "Subscribe", + "checkStatusButton": "Check Status", + "subscribed": "Subscribed", + "disconnectPopconfirm": { + "title": "Are you sure you want to disconnect your Telegram account?", + "description": "You will no longer receive notifications via Telegram" + }, + "verificationPopover": { + "title": "Verification", + "description": "Add the bot on your telegram account and send the command {{command}} to the bot" + } + } }, "scanners": { "column": { diff --git a/src/Components/MyIcon/index.js b/src/Components/MyIcon/index.js index 8fd232a..35149d1 100644 --- a/src/Components/MyIcon/index.js +++ b/src/Components/MyIcon/index.js @@ -1,7 +1,9 @@ import { CopyOutlined, + DisconnectOutlined, EyeInvisibleOutlined, EyeOutlined, + ReloadOutlined, } from "@ant-design/icons"; import { Tooltip } from "antd"; import { useTranslation } from "react-i18next"; @@ -37,3 +39,13 @@ export function MyShowHiddenIcon({ setIsHidden, isHidden }) { ); } + +export function MyReloadIcon({ onClick }) { + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/src/Pages/UserProfile/index.js b/src/Pages/UserProfile/index.js index 779bd6f..d8f29bd 100644 --- a/src/Pages/UserProfile/index.js +++ b/src/Pages/UserProfile/index.js @@ -5,6 +5,7 @@ import { Form, Input, Popconfirm, + Popover, Row, Select, Space, @@ -14,7 +15,7 @@ import { Upload, notification, } from "antd"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Constants, EncodeStringToBase64, @@ -24,18 +25,29 @@ import { hasPermission, isEmailValid, myFetch, + myFetchContentType, wsConnectionCustomEventName, } from "../../utils"; import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { FileTextOutlined, KeyOutlined } from "@ant-design/icons"; +import { Trans, useTranslation } from "react-i18next"; +import { + DisconnectOutlined, + FileTextOutlined, + InfoCircleOutlined, + KeyOutlined, + NotificationOutlined, +} from "@ant-design/icons"; import { MyUserAvatar } from "../../Components/MyAvatar"; import { useUserProfileContext } from "../../Contexts/UserProfileContext"; import { useWebSocketContext } from "../../Contexts/WebSocketContext"; import { useAppContext } from "../../Contexts/AppContext"; import { useSideBarContext } from "../../Contexts/SideBarContext"; import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler"; -import { MyCopyIcon, MyShowHiddenIcon } from "../../Components/MyIcon"; +import { + MyCopyIcon, + MyReloadIcon, + MyShowHiddenIcon, +} from "../../Components/MyIcon"; export default function UserProfile() { const webSocketContext = useWebSocketContext(); @@ -53,6 +65,13 @@ export default function UserProfile() { const [repeatedNewPassword, setRepeatedNewPassword] = useState(""); const [newApiKeyName, setNewApikeyName] = useState(""); const [showApiKeyPassword, setShowApiKeyPassword] = useState(false); + const [ + telegramNotificationSubscribedStatus, + setTelegramNotificationSubscribedStatus, + ] = useState(null); + + const statusInterval = useRef(null); + const telegramVerifyCodeRequestTime = useRef(null); const getSessionTableColumns = () => { return [ @@ -271,6 +290,165 @@ export default function UserProfile() { setNewApikeyName(""); }; + const telegramVerifyStatusRequest = (updateOnlyOnVerified) => + myFetch( + `/status/${appContext.userId.current}`, + "GET", + null, + {}, + myFetchContentType.JSON, + Constants.TELEGRAM_BOT_MANAGER_ADDRESS + ).then((data) => { + // manual check verify status by clicking reload icon + if (!updateOnlyOnVerified) { + setTelegramNotificationSubscribedStatus(data.Status); + } + + // used for auto check verify status + if (data.Status === true) { + setTelegramNotificationSubscribedStatus(data.Status); + clearInterval(statusInterval.current); + telegramVerifyCodeRequestTime.current = null; + } + }); + + const TelegramNotificationSubscribedStatusItem = () => { + // check verified status + if (telegramNotificationSubscribedStatus === null) { + return ( + + ); + } + + // unsubscribe + if (telegramNotificationSubscribedStatus === true) { + return ( + + {t("userProfile.telegram.subscribed")} + + { + myFetch( + `/verifycode/${appContext.userId.current}`, + "DELETE", + null, + {}, + myFetchContentType.JSON, + Constants.TELEGRAM_BOT_MANAGER_ADDRESS + ).then(() => { + setTelegramNotificationSubscribedStatus(null); + }); + }} + > + + + + ); + } + + // subscribe + if (telegramNotificationSubscribedStatus === false) { + return ( + + ); + } + + // verification code + return ( + + + /verify ${telegramNotificationSubscribedStatus}`, + }} + /> + +
+ +
+
+ } + > + + + + {telegramNotificationSubscribedStatus} + + + + telegramVerifyStatusRequest()} /> + + ); + }; + + useEffect(() => { + if ( + telegramNotificationSubscribedStatus !== true && + telegramNotificationSubscribedStatus !== false && + telegramNotificationSubscribedStatus !== null + ) { + telegramVerifyCodeRequestTime.current = new Date(); + + statusInterval.current = setInterval(() => { + // check expiration time + if ( + telegramVerifyCodeRequestTime.current !== null && + new Date() - telegramVerifyCodeRequestTime.current > + Constants.GLOBALS.TELEGRAM_VERIFY_CODE_EXPIRATION_TIME + ) { + clearInterval(statusInterval.current); + telegramVerifyCodeRequestTime.current = null; + setTelegramNotificationSubscribedStatus(false); + return; + } + + telegramVerifyStatusRequest(true); + }, 1000); + } + + return () => clearInterval(statusInterval.current); + }, [telegramNotificationSubscribedStatus]); + useEffect(() => { const userProfileRequest = () => myFetch("/user/profile", "GET").then((data) => { @@ -342,6 +520,10 @@ export default function UserProfile() { onChange={(e) => i18n.changeLanguage(e)} /> + + + + diff --git a/src/utils.js b/src/utils.js index 98e5270..d31fb9a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -14,6 +14,8 @@ let wsAddress = ""; let logApiAddress = ""; let roboticsApiAddress = ""; var roboticsSwaggerAddress = ""; +var telegramBotManagerAddress = ""; +var telegramBotManagerStaticContentAddress = ""; if (window.location.hostname === "localhost" && window.location.port === "") { // for docker container testing on localhost @@ -23,6 +25,8 @@ if (window.location.hostname === "localhost" && window.location.port === "") { logApiAddress = "http://localhost/lm/v1/log"; roboticsApiAddress = "http://localhost/rcm/v1"; roboticsSwaggerAddress = "http://localhost/rcm/swagger/index.html"; + telegramBotManagerAddress = "http://localhost/tnm/v1"; + telegramBotManagerStaticContentAddress = "http://localhost/tnm/"; } else if (window.location.hostname === "localhost") { // programming on localhost apiAddress = "http://localhost:50050/v1"; @@ -31,6 +35,8 @@ if (window.location.hostname === "localhost" && window.location.port === "") { logApiAddress = "http://127.0.0.1:50110/v1/log"; roboticsApiAddress = "http://localhost:50055/v1"; roboticsSwaggerAddress = "http://localhost:50055/swagger/index.html"; + telegramBotManagerAddress = "http://localhost:50056/v1"; + telegramBotManagerStaticContentAddress = "http://localhost:50056/"; /*} else if (window.location.hostname === "192.168.178.93") { apiAddress = "http://192.168.178.93:50050/v1"; staticContentAddress = "http://192.168.178.93:50050/"; @@ -43,6 +49,8 @@ if (window.location.hostname === "localhost" && window.location.port === "") { logApiAddress = `${window.location.protocol}${window.location.hostname}/lm/v1/log`; roboticsApiAddress = `${window.location.protocol}${window.location.hostname}/rcm/v1`; roboticsSwaggerAddress = `${window.location.protocol}${window.location.hostname}/rcm/swagger/index.html`; + telegramBotManagerAddress = `${window.location.protocol}${window.location.hostname}/tnm/v1`; + telegramBotManagerStaticContentAddress = `${window.location.protocol}${window.location.hostname}/tnm/`; } export const Constants = { @@ -66,6 +74,8 @@ export const Constants = { LOG_API_ADDRESS: logApiAddress, ROBOTICS_API_ADDRESS: roboticsApiAddress, // robot-control-manager ROBOTICS_SWAGGER_ADDRESS: roboticsSwaggerAddress, + TELEGRAM_BOT_MANAGER_ADDRESS: telegramBotManagerAddress, + TELEGRAM_BOT_MANAGER_CONTENT_ADDRESS: telegramBotManagerStaticContentAddress, ROUTE_PATHS: { EQUIPMENT_DOCUMENTATION: "/equipment-documentation", EQUIPMENT_DOCUMENTATION_VIEW: "/equipment-documentation/", @@ -113,6 +123,7 @@ export const Constants = { MAX_USER_API_KEY_NAME_LENGTH: 30, ROBOTICS_ROBOTS_PAGINATION_LIMIT: 5, ROBOTICS_UNAUTHORIZED_PAGINATION_LIMIT: 5, + TELEGRAM_VERIFY_CODE_EXPIRATION_TIME: 10 * 60 * 1000, // 10 minutes }, MAX_AVATAR_SIZE: 5 * 1024 * 1024, ACCEPTED_AVATAR_FILE_TYPES: [