// ==UserScript==
// @name KG_Chat_Application
// @namespace klavogonki
// @version 1.0.6
// @description Enhance the chat abilities
// @author Patcher
// @match *://klavogonki.ru/*
// @exclude https://klavogonki.ru/g/?gmid=*
// @exclude https://klavogonki.ru/chatlogs/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=klavogonki.ru
// @grant none
// ==/UserScript==
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./main.js":
/*!*****************!*\
!*** ./main.js ***!
\*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _src_definitions_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./src/definitions.js */ \"./src/definitions.js\");\n/* harmony import */ var _src_xmppConnection_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./src/xmppConnection.js */ \"./src/xmppConnection.js\");\n/* harmony import */ var _src_userManager_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./src/userManager.js */ \"./src/userManager.js\");\n/* harmony import */ var _src_messageManager_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./src/messageManager.js */ \"./src/messageManager.js\");\n/* harmony import */ var _src_chatUI_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./src/chatUI.js */ \"./src/chatUI.js\");\n/* harmony import */ var _src_chatFeatures_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./src/chatFeatures.js */ \"./src/chatFeatures.js\");\n/* harmony import */ var _src_events_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./src/events.js */ \"./src/events.js\");\n/* harmony import */ var _src_xmppClient_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./src/xmppClient.js */ \"./src/xmppClient.js\");\n/* harmony import */ var _src_helpers_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./src/helpers.js */ \"./src/helpers.js\");\n/* harmony import */ var _src_auth_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./src/auth.js */ \"./src/auth.js\");\n/* harmony import */ var _src_components_helpPanel_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./src/components/helpPanel.js */ \"./src/components/helpPanel.js\");\n/* harmony import */ var _src_styles_emojiPanel_css__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./src/styles/emojiPanel.css */ \"./src/styles/emojiPanel.css\");\n/* harmony import */ var _src_styles_helpPanel_css__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./src/styles/helpPanel.css */ \"./src/styles/helpPanel.css\");\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n// Function to detect if running in an iframe\r\nfunction isInIframe() {\r\n try {\r\n return window !== window.top;\r\n } catch (e) {\r\n // If there's an error when trying to access window.top, \r\n // it's likely due to cross-origin restrictions, which means we're in an iframe\r\n return true;\r\n }\r\n}\r\n\r\n// ------------------------- Auth Check ---------------------------\r\nfunction checkAuth() {\r\n // First check if running in iframe\r\n if (isInIframe()) {\r\n console.error('Application cannot run in an iframe');\r\n return false;\r\n }\r\n\r\n const params = new URLSearchParams(window.location.search);\r\n if (window.location.pathname === '/g/' && params.has('gmid')) {\r\n return false;\r\n }\r\n if (window.location.href.includes('/gamelist/')) {\r\n (0,_src_auth_js__WEBPACK_IMPORTED_MODULE_9__.getAuthData)();\r\n return false;\r\n }\r\n const authData = localStorage.getItem('klavoauth');\r\n if (!authData || !_src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.config.username || !_src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.config.password) {\r\n localStorage.removeItem('klavoauth');\r\n window.location.href = 'https://klavogonki.ru/gamelist/';\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n// ------------------------- Main App ---------------------------\r\nasync function initializeApp() {\r\n try {\r\n // Add viewport meta tag\r\n (0,_src_helpers_js__WEBPACK_IMPORTED_MODULE_8__.addViewportMeta)();\r\n\r\n if (!checkAuth()) return;\r\n\r\n // Initialize UI and features\r\n (0,_src_chatUI_js__WEBPACK_IMPORTED_MODULE_4__.createChatUI)();\r\n (0,_src_chatFeatures_js__WEBPACK_IMPORTED_MODULE_5__.addChatToggleFeature)();\r\n (0,_src_events_js__WEBPACK_IMPORTED_MODULE_6__.setupDragHandlers)();\r\n (0,_src_events_js__WEBPACK_IMPORTED_MODULE_6__.setupResizeHandlers)();\r\n (0,_src_events_js__WEBPACK_IMPORTED_MODULE_6__.setupWindowResizeHandler)();\r\n\r\n // Set up the messages panel observer\r\n (0,_src_helpers_js__WEBPACK_IMPORTED_MODULE_8__.observeMessagesPanel)();\r\n\r\n // Initialize managers and XMPP connection\r\n const userManager = new _src_userManager_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"]('user-list');\r\n const messageManager = new _src_messageManager_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"]('messages-panel', (0,_src_helpers_js__WEBPACK_IMPORTED_MODULE_8__.parseUsername)(_src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.config.username));\r\n const xmppConnection = new _src_xmppConnection_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]({\r\n username: _src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.config.username,\r\n password: _src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.config.password,\r\n bindUrl: _src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.XMPP_BIND_URL,\r\n delay: _src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.delay\r\n });\r\n\r\n const xmppClient = (0,_src_xmppClient_js__WEBPACK_IMPORTED_MODULE_7__.createXMPPClient)(\r\n xmppConnection,\r\n userManager,\r\n messageManager,\r\n _src_definitions_js__WEBPACK_IMPORTED_MODULE_0__.config.username\r\n );\r\n\r\n // Message sending setup\r\n const input = document.getElementById('message-input');\r\n const sendMessage = () => {\r\n const text = input.value.trim();\r\n if (!text) return;\r\n xmppClient.sendMessage(text);\r\n input.value = '';\r\n input.focus();\r\n };\r\n\r\n document.getElementById('send-button').addEventListener('click', sendMessage);\r\n input.addEventListener('keypress', e => e.key === 'Enter' && sendMessage());\r\n\r\n // Set up private messaging events\r\n (0,_src_helpers_js__WEBPACK_IMPORTED_MODULE_8__.setupPrivateMessageEvents)();\r\n // New: Set up help command events (similar to /pm command)\r\n _src_components_helpPanel_js__WEBPACK_IMPORTED_MODULE_10__.HelpPanel.setupHelpCommandEvents();\r\n\r\n // Connect to XMPP and join the room\r\n await xmppClient.connect();\r\n window.xmppClient = xmppClient; // For debugging\r\n\r\n // Detect /pm username command and activate private mode, and /exit to exit private mode\r\n input.addEventListener('input', function (event) {\r\n (0,_src_helpers_js__WEBPACK_IMPORTED_MODULE_8__.handlePrivateMessageInput)(event.target);\r\n });\r\n } catch (error) {\r\n console.error('App init error:', error);\r\n localStorage.removeItem('klavoauth');\r\n window.location.href = 'https://klavogonki.ru/gamelist/';\r\n }\r\n}\r\n\r\n// Start the app\r\ninitializeApp();\n\n//# sourceURL=webpack://tampermonkey-script/./main.js?");
/***/ }),
/***/ "./node_modules/css-loader/dist/cjs.js!./src/styles/emojiPanel.css":
/*!*************************************************************************!*\
!*** ./node_modules/css-loader/dist/cjs.js!./src/styles/emojiPanel.css ***!
\*************************************************************************/
/***/ ((module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `:root {\r\n --emoji-font: \"Noto Color Emoji\";\r\n}\r\n\r\n.emoji-panel {\r\n opacity: 0;\r\n transition: opacity 0.3s ease;\r\n position: absolute !important;\r\n top: 45% !important;\r\n left: 50% !important;\r\n transform: translate(-50%, -50%) !important;\r\n background: #1e1e1e !important;\r\n border: 1px solid #333 !important;\r\n border-radius: 0.4em !important;\r\n width: 380px;\r\n height: 580px;\r\n display: flex;\r\n flex-direction: column;\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;\r\n z-index: 1000;\r\n}\r\n\r\n.emoji-search-container {\r\n padding: 1em !important;\r\n border: none !important;\r\n}\r\n\r\n.emoji-search {\r\n width: 100%;\r\n padding: 8px;\r\n border-radius: 4px;\r\n background: #2a2a2a !important;\r\n border: none !important;\r\n border-radius: 0.2em !important;\r\n color: #deb887 !important;\r\n caret-color: #deb887 !important;\r\n font-size: 0.9em !important;\r\n}\r\n\r\n.emoji-search:focus {\r\n outline: none;\r\n border-color: #666;\r\n}\r\n\r\n.emoji-categories {\r\n position: sticky !important;\r\n top: 0 !important;\r\n display: grid !important;\r\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)) !important;\r\n padding: 8px !important;\r\n border-bottom: 1px solid #333 !important;\r\n gap: 8px !important;\r\n justify-content: center !important;\r\n align-items: center !important;\r\n overflow-x: auto !important;\r\n scrollbar-width: thin !important;\r\n}\r\n\r\n.emoji-category-btn {\r\n font-family: var(--emoji-font) !important;\r\n position: relative !important;\r\n background: none !important;\r\n border: none !important;\r\n padding: 4px !important;\r\n cursor: pointer !important;\r\n font-size: 1.5em !important;\r\n transition: background-color 0.2s;\r\n width: 100% !important;\r\n height: 100% !important;\r\n display: flex !important;\r\n align-items: center !important;\r\n justify-content: center !important;\r\n aspect-ratio: 1 !important;\r\n border-bottom: 3px solid transparent !important;\r\n}\r\n\r\n/* Active category button gets a 3px {color} border */\r\n.emoji-category-btn.active {\r\n opacity: 1;\r\n border-bottom: 3px solid goldenrod !important;\r\n}\r\n\r\n.emoji-category-btn:hover {\r\n background-color: #333;\r\n}\r\n\r\n.emoji-container {\r\n flex: 1;\r\n overflow-y: auto;\r\n overflow-x: hidden !important;\r\n display: grid !important;\r\n gap: 8px !important;\r\n width: 100% !important;\r\n max-width: 100% !important;\r\n scrollbar-width: none !important;\r\n}\r\n\r\n.emoji-category-section {\r\n margin-bottom: 10px;\r\n}\r\n\r\n.emoji-category-header {\r\n padding: 8px !important;\r\n color: #deb887 !important;\r\n font-size: 0.9em !important;\r\n position: sticky !important;\r\n top: 0px !important;\r\n background: #1e1e1e !important;\r\n z-index: 1 !important;\r\n border-bottom: 1px solid #333 !important;\r\n width: 100% !important;\r\n box-sizing: border-box !important;\r\n margin: 0 !important;\r\n}\r\n\r\n.emoji-list {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)) !important;\r\n padding: 8px !important;\r\n gap: 8px;\r\n align-content: start;\r\n}\r\n\r\n.emoji-btn {\r\n font-family: var(--emoji-font), sans-serif !important;\r\n background: none;\r\n border: none;\r\n padding: 4px !important;\r\n cursor: pointer;\r\n font-size: 1.5em !important;\r\n transition: background-color 0.2s;\r\n width: 100% !important;\r\n height: 100% !important;\r\n display: flex !important;\r\n align-items: center !important;\r\n justify-content: center !important;\r\n aspect-ratio: 1 !important;\r\n}\r\n\r\n.emoji-btn:hover {\r\n background-color: #333;\r\n}\r\n\r\n.emoji-footer {\r\n border-top: 1px solid #333 !important;\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: space-between;\r\n align-items: center;\r\n padding: 5px;\r\n}\r\n\r\n/* Info panel fixed at bottom with a 50px height */\r\n.emoji-info-panel {\r\n height: 40px !important;\r\n padding: 8px !important;\r\n display: flex !important;\r\n align-items: center !important;\r\n gap: 8px !important;\r\n color: #deb887 !important;\r\n font-size: 0.9em !important;\r\n background: #1e1e1e !important;\r\n}\r\n\r\n.emoji-language-select {\r\n border-radius: 0.4em !important;\r\n /* Rounded corners */\r\n padding: 5px 10px;\r\n /* Space inside the select */\r\n font-size: 14px;\r\n /* Font size for readability */\r\n background-color: #2a2a2a !important;\r\n /* Dark background to match search input */\r\n border: 1px solid #444 !important;\r\n /* Subtle dark border */\r\n color: #deb887 !important;\r\n /* Text color to match headers and info panel */\r\n cursor: pointer;\r\n /* Pointer cursor to indicate it's clickable */\r\n transition: border-color 0.3s ease;\r\n /* Smooth transition for border color */\r\n}\r\n\r\n/* Focus state */\r\n.emoji-language-select:focus {\r\n outline: none;\r\n /* Remove default outline */\r\n border-color: goldenrod !important;\r\n /* Match active category button */\r\n}\r\n\r\n/* Hover state */\r\n.emoji-language-select:hover {\r\n border-color: #666 !important;\r\n /* Lighten border on hover */\r\n}\r\n\r\n/* Optional: Style for options (limited control) */\r\n.emoji-language-select option {\r\n font-size: 14px;\r\n /* Match font size */\r\n background-color: #2a2a2a !important;\r\n /* Dark background for options */\r\n color: #deb887 !important;\r\n /* Text color for options */\r\n}\r\n\r\n.emoji-info-icon {\r\n font-family: var(--emoji-font) !important;\r\n font-size: 1.5em !important;\r\n}\r\n\r\n.emoji-info-keywords {\r\n color: #888 !important;\r\n font-style: italic !important;\r\n}\r\n\r\n/* Scrollbar styling */\r\n.emoji-container::-webkit-scrollbar,\r\n.emoji-categories::-webkit-scrollbar {\r\n width: 6px;\r\n height: 6px;\r\n}\r\n\r\n.emoji-container::-webkit-scrollbar-track,\r\n.emoji-categories::-webkit-scrollbar-track {\r\n background: #1e1e1e;\r\n}\r\n\r\n.emoji-container::-webkit-scrollbar-thumb,\r\n.emoji-categories::-webkit-scrollbar-thumb {\r\n background: #444;\r\n border-radius: 3px;\r\n}\r\n\r\n.emoji-container::-webkit-scrollbar-thumb:hover,\r\n.emoji-categories::-webkit-scrollbar-thumb:hover {\r\n background: #555;\r\n}`, \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/styles/emojiPanel.css?./node_modules/css-loader/dist/cjs.js");
/***/ }),
/***/ "./node_modules/css-loader/dist/cjs.js!./src/styles/helpPanel.css":
/*!************************************************************************!*\
!*** ./node_modules/css-loader/dist/cjs.js!./src/styles/helpPanel.css ***!
\************************************************************************/
/***/ ((module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `.help-panel {\r\n opacity: 0;\r\n transition: opacity 0.3s ease;\r\n position: fixed;\r\n left: 50%;\r\n top: 50%;\r\n transform: translate(-50%, -50%);\r\n height: fit-content;\r\n max-height: 70vh !important;\r\n width: fit-content;\r\n overflow-y: auto;\r\n scrollbar-width: none;\r\n /* Existing styles */\r\n background: #1e1e1e;\r\n border: 1px solid #333 !important;\r\n padding: 1em;\r\n border-radius: 0.4em !important;\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;\r\n color: #deb887 !important;\r\n font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;\r\n z-index: 1100;\r\n}\r\n\r\n/* Header styling */\r\n.help-header {\r\n margin-top: 0;\r\n font-size: 1.5em;\r\n text-align: center;\r\n color: #ffa500 !important;\r\n}\r\n\r\n/* Subheader styling – now further differentiated */\r\n.help-subheader {\r\n margin: 0.8em 0 0.3em 1em;\r\n /* Added left margin of 1em */\r\n font-size: 1.1em;\r\n /* Smaller than header */\r\n color: #ffcc00 !important;\r\n font-weight: normal;\r\n text-align: left;\r\n}\r\n\r\n/* Header for a specific section */\r\n.help-section-header {\r\n margin-top: 0 !important;\r\n margin-bottom: 1.5em !important;\r\n font-size: 1.5em !important;\r\n text-align: center;\r\n color: #ffa500 !important;\r\n}\r\n\r\n/* Subheader for a specific section (different type) */\r\n.help-section-subheader {\r\n margin: 0.8em 0 1.2em 1em;\r\n /* left margin added */\r\n font-size: 0.9em;\r\n color: #ffcc00 !important;\r\n font-weight: normal;\r\n text-align: left;\r\n}\r\n\r\n/* List styling */\r\n.help-list {\r\n list-style-type: none;\r\n padding-left: 0;\r\n}\r\n\r\n/* List item styling */\r\n.help-list-item {\r\n display: flex;\r\n align-items: center;\r\n flex-direction: row;\r\n padding: 0.3em;\r\n border-bottom: 1px dashed #444 !important;\r\n}\r\n\r\n.help-list-item:last-child {\r\n border-bottom: none;\r\n}\r\n\r\n/* Hotkey text styling – updated to force command description wrap */\r\n.help-hotkey {\r\n width: fit-content;\r\n display: flex;\r\n color: #7ed4ff !important;\r\n font-family: monospace;\r\n background-color: rgba(126, 212, 255, 0.1) !important;\r\n border: 1px solid rgba(126, 212, 255, 0.4) !important;\r\n border-left: 3px solid rgba(126, 212, 255, 0.4) !important;\r\n border-radius: 0 0.2em 0.2em 0 !important;\r\n padding: 2px 4px;\r\n margin-right: 1em;\r\n}`, \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/styles/helpPanel.css?./node_modules/css-loader/dist/cjs.js");
/***/ }),
/***/ "./node_modules/css-loader/dist/cjs.js!./src/styles/style.css":
/*!********************************************************************!*\
!*** ./node_modules/css-loader/dist/cjs.js!./src/styles/style.css ***!
\********************************************************************/
/***/ ((module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n___CSS_LOADER_EXPORT___.push([module.id, \"@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap);\"]);\n___CSS_LOADER_EXPORT___.push([module.id, \"@import url(https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap);\"]);\n___CSS_LOADER_EXPORT___.push([module.id, \"@import url(https://fonts.googleapis.com/css2?family=Iansui&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap);\"]);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `/* Variables */\r\n:root {\r\n --border-radius: 0.2em;\r\n --min-chat-width: 320px;\r\n --min-chat-height: 200px;\r\n --user-list-width: fit-content;\r\n --emoji-font: \"Noto Color Emoji\";\r\n}\r\n\r\n/* Main chat container */\r\n#app-chat-container {\r\n border-radius: 0.4em 0.4em 0 0 !important;\r\n position: fixed;\r\n bottom: 0;\r\n left: 0;\r\n height: 300px;\r\n background: #1e1e1e !important;\r\n border: 1px solid #333 !important;\r\n display: flex;\r\n font-family: sans-serif;\r\n color: #deb887 !important;\r\n z-index: 999;\r\n min-width: var(--min-chat-width) !important;\r\n min-height: var(--min-chat-height) !important;\r\n box-sizing: border-box;\r\n max-width: 100vw;\r\n overflow: hidden;\r\n transition: opacity 0.3s ease, transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);\r\n}\r\n\r\n#app-chat-container a {\r\n color: #82B32A !important;\r\n transition: color 0.15s !important;\r\n}\r\n\r\n#app-chat-container a:hover {\r\n color: #95cc30 !important;\r\n}\r\n\r\n/* Chat container states */\r\n#app-chat-container.maximized {\r\n position: fixed;\r\n z-index: 1010;\r\n}\r\n\r\n#app-chat-container:not(.visible-chat):not(.hidden-chat):not(.maximized):not(.floating-chat) {\r\n display: none;\r\n opacity: 0;\r\n}\r\n\r\n#app-chat-container.visible-chat {\r\n transform: translateY(0) !important;\r\n}\r\n\r\n#app-chat-container.hidden-chat {\r\n opacity: 1;\r\n transform: translateY(calc(100% - 25px)) !important;\r\n}\r\n\r\n#app-chat-container.floating-chat {\r\n border-radius: 0.4em !important;\r\n}\r\n\r\n/* Responsive styles */\r\n@media (max-width: 780px) {\r\n #app-chat-container .chat-wrapper {\r\n width: 100% !important;\r\n border-right: none !important;\r\n }\r\n}\r\n\r\n@media screen and (max-width: 768px),\r\n(hover: none),\r\n(pointer: coarse) {\r\n body {\r\n background-color: #1e1e1e !important;\r\n }\r\n\r\n #app-chat-container {\r\n height: 100% !important;\r\n width: 100vw !important;\r\n min-height: 100% !important;\r\n min-width: 100vw !important;\r\n border: none !important;\r\n border-radius: 0 !important;\r\n overflow: hidden !important;\r\n }\r\n\r\n /* Hide non-essential elements on touch devices */\r\n #app-chat-container .resize-handle,\r\n #app-chat-container .font-size-control,\r\n #app-chat-container .header-button,\r\n #app-chat-container #send-button,\r\n /* Unnecessary elements of klavogonki page */\r\n .userpanel,\r\n #userpanel-dummy,\r\n #reformal_tab,\r\n .ownbanner-back,\r\n .feedback,\r\n .bar,\r\n #content,\r\n #head,\r\n #index,\r\n #footer,\r\n #google_esf {\r\n display: none !important;\r\n }\r\n}\r\n\r\n@media screen and (max-width: 367px) {\r\n #app-chat-container .video-container {\r\n transform-origin: left !important;\r\n transform: scale(0.9) !important;\r\n }\r\n}\r\n\r\n@media screen and (max-width: 350px) {\r\n #app-chat-container .video-container {\r\n transform-origin: left !important;\r\n transform: scale(0.8) !important;\r\n }\r\n}\r\n\r\n/* Font size control */\r\n#app-chat-container .font-size-control {\r\n position: absolute !important;\r\n top: 0 !important;\r\n left: 0 !important;\r\n display: flex !important;\r\n align-items: center !important;\r\n justify-content: center !important;\r\n height: 25px !important;\r\n padding: 0 10px !important;\r\n gap: 5px !important;\r\n z-index: 6 !important;\r\n}\r\n\r\n#app-chat-container .font-size-label {\r\n color: #deb887 !important;\r\n cursor: default !important;\r\n user-select: none !important;\r\n}\r\n\r\n#app-chat-container .font-size-label.small {\r\n font-size: 0.8em !important;\r\n}\r\n\r\n#app-chat-container .font-size-label.large {\r\n font-size: 1.2em !important;\r\n}\r\n\r\n#app-chat-container .font-size-slider {\r\n width: 80px !important;\r\n height: 4px !important;\r\n -webkit-appearance: none !important;\r\n appearance: none !important;\r\n background: #333 !important;\r\n outline: none !important;\r\n border-radius: 2px !important;\r\n transition: opacity 0.2s !important;\r\n}\r\n\r\n#app-chat-container .font-size-slider::-webkit-slider-thumb {\r\n -webkit-appearance: none !important;\r\n appearance: none !important;\r\n width: 10px !important;\r\n height: 10px !important;\r\n border-radius: 50% !important;\r\n background: #deb887 !important;\r\n cursor: pointer !important;\r\n}\r\n\r\n#app-chat-container .font-size-slider::-moz-range-thumb {\r\n width: 10px !important;\r\n height: 10px !important;\r\n border-radius: 50% !important;\r\n background: #deb887 !important;\r\n cursor: pointer !important;\r\n border: none !important;\r\n}\r\n\r\n/* Resize handles */\r\n#app-chat-container .resize-handle {\r\n position: absolute !important;\r\n background: transparent !important;\r\n z-index: 1000 !important;\r\n}\r\n\r\n#app-chat-container .resize-handle.top {\r\n top: -3px !important;\r\n left: 0 !important;\r\n right: 0 !important;\r\n height: 6px !important;\r\n cursor: ns-resize !important;\r\n}\r\n\r\n#app-chat-container .resize-handle.left {\r\n left: -3px !important;\r\n top: 0 !important;\r\n bottom: 0 !important;\r\n width: 6px !important;\r\n cursor: ew-resize !important;\r\n}\r\n\r\n#app-chat-container .resize-handle.right {\r\n right: -3px !important;\r\n top: 0 !important;\r\n bottom: 0 !important;\r\n width: 6px !important;\r\n cursor: ew-resize !important;\r\n}\r\n\r\n/* Chat wrapper: two-column layout */\r\n#app-chat-container .chat-wrapper {\r\n display: flex !important;\r\n flex-direction: row !important;\r\n flex: 1 !important;\r\n min-width: var(--min-chat-width) !important;\r\n overflow: hidden !important;\r\n}\r\n\r\n/* Left column: messages & input */\r\n#app-chat-container .chat-content {\r\n margin-top: 25px !important;\r\n background: #1e1e1e !important;\r\n display: flex !important;\r\n flex-direction: column !important;\r\n flex: 1 !important;\r\n overflow: hidden !important;\r\n}\r\n\r\n/* Scrollable container settings */\r\n#app-chat-container .messages-panel {\r\n flex: 1 !important;\r\n overflow-y: auto !important;\r\n overflow-x: hidden !important;\r\n padding: 1em !important;\r\n display: flex !important;\r\n flex-direction: column !important;\r\n gap: 0.2em !important;\r\n scrollbar-width: thin !important;\r\n scrollbar-color: #333 #1e1e1e !important;\r\n}\r\n\r\n/* Custom scrollbar styling for WebKit browsers */\r\n#app-chat-container .messages-panel::-webkit-scrollbar {\r\n width: 8px !important;\r\n}\r\n\r\n#app-chat-container .messages-panel::-webkit-scrollbar-thumb {\r\n background-color: #333 !important;\r\n}\r\n\r\n#app-chat-container .messages-panel::-webkit-scrollbar-thumb:hover {\r\n background-color: #444 !important;\r\n}\r\n\r\n#app-chat-container .messages-panel::-webkit-scrollbar-track {\r\n background-color: #1e1e1e !important;\r\n}\r\n\r\n/* Input container at bottom */\r\n#app-chat-container .input-container {\r\n display: flex !important;\r\n align-items: center !important;\r\n padding: 1em !important;\r\n gap: 0.5em !important;\r\n border-top: 1px solid #333 !important;\r\n}\r\n\r\n#app-chat-container #message-input {\r\n outline: none !important;\r\n flex: 1 !important;\r\n background: #2a2a2a !important;\r\n color: #deb887 !important;\r\n padding: 0.5em !important;\r\n border-radius: var(--border-radius) !important;\r\n min-width: 0 !important;\r\n border: none !important;\r\n position: relative;\r\n font-family: inherit !important;\r\n /* Remove any fixed font-size to properly inherit from container */\r\n line-height: normal !important;\r\n transition: font-size 0.2s ease !important;\r\n}\r\n\r\n#app-chat-container .length-field-popup {\r\n position: absolute !important;\r\n display: flex !important;\r\n font-size: 12px !important;\r\n font-weight: bold !important;\r\n font-family: \"Montserrat\", Iansui, sans-serif !important;\r\n bottom: 60px !important;\r\n transition: left 100ms ease-out !important;\r\n height: 20px !important;\r\n align-items: center !important;\r\n justify-content: center !important;\r\n opacity: 0;\r\n border: none !important;\r\n}\r\n\r\n.bounce-in {\r\n animation: bounceIn 500ms forwards;\r\n}\r\n\r\n@keyframes bounceIn {\r\n 0% {\r\n transform: translateY(0);\r\n opacity: 0;\r\n }\r\n\r\n 50% {\r\n transform: translateY(-10px);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: translateY(0);\r\n opacity: 1;\r\n }\r\n}\r\n\r\n.bounce-out {\r\n animation: bounceOut 500ms forwards;\r\n}\r\n\r\n@keyframes bounceOut {\r\n 0% {\r\n transform: translateY(0);\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n transform: translateY(-10px);\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n transform: translateY(0);\r\n opacity: 0;\r\n }\r\n}\r\n\r\n#app-chat-container #message-input.private-mode {\r\n background-color: #ff6b6b38 !important;\r\n color: #ff6b6b !important;\r\n caret-color: #ff6b6b !important;\r\n}\r\n\r\n#app-chat-container #message-input.private-mode::placeholder {\r\n color: #ff6b6b99;\r\n}\r\n\r\n#app-chat-container #messages-panel.private-mode::after {\r\n content: \"🔒\";\r\n position: absolute;\r\n right: 5px;\r\n top: 5px;\r\n font-size: 10px;\r\n opacity: 0.5;\r\n}\r\n\r\n/* Right column: user list container */\r\n#app-chat-container .user-list-container {\r\n margin-top: 25px;\r\n width: var(--user-list-width) !important;\r\n min-width: 180px !important;\r\n max-width: var(--user-list-width) !important;\r\n overflow-y: auto !important;\r\n overflow-x: hidden !important;\r\n padding: 1em !important;\r\n background: #1e1e1e !important;\r\n border-left: 1px solid #333 !important;\r\n}\r\n\r\n.reveal-userlist-btn {\r\n position: absolute !important;\r\n top: 50% !important;\r\n right: -1px !important;\r\n transform: translateY(-50%) !important;\r\n z-index: 1000 !important;\r\n display: flex !important;\r\n align-items: center !important;\r\n justify-content: center !important;\r\n padding: 0.4em !important;\r\n background: #222 !important;\r\n font-size: 18px !important;\r\n font-family: var(--emoji-font), sans-serif !important;\r\n font-weight: bold !important;\r\n border: 1px solid #333 !important;\r\n border-radius: 0.4em 0 0 0.4em !important;\r\n cursor: pointer !important;\r\n transition: background 0.2s ease, opacity 0.2s ease !important;\r\n}\r\n\r\n/* Hover effect */\r\n.reveal-userlist-btn:hover {\r\n background: #333 !important;\r\n}\r\n\r\n/* When user list is hidden */\r\n.hidden-userlist {\r\n display: flex !important;\r\n}\r\n\r\n/* When user list is shown */\r\n.shown-userlist {\r\n display: none !important;\r\n}\r\n\r\n/* Message styles */\r\n#app-chat-container .message {\r\n padding: 0.2em 0.4em !important;\r\n display: flex;\r\n flex-direction: row;\r\n border-radius: var(--border-radius) !important;\r\n width: fit-content !important;\r\n max-width: 100% !important;\r\n word-break: break-word !important;\r\n}\r\n\r\n#app-chat-container .message-text .emoji-adjuster {\r\n font-family: var(--emoji-font), sans-serif !important;\r\n font-size: 1.5em !important;\r\n margin: 0 0.1em !important;\r\n display: inline-flex !important;\r\n}\r\n\r\n#app-chat-container .message-text .mention {\r\n display: inline-flex !important;\r\n font-family: Montserrat !important;\r\n font-weight: 500 !important;\r\n}\r\n\r\n#app-chat-container .message.system {\r\n background-color: #ffa50020 !important;\r\n border: 1px solid #ffa60030 !important;\r\n border-left: 3px solid #ffa500 !important;\r\n}\r\n\r\n#app-chat-container .message.system .time {\r\n margin-right: unset !important;\r\n color: #ffa50060 !important;\r\n}\r\n\r\n#app-chat-container .message.system .username {\r\n display: none !important;\r\n}\r\n\r\n#app-chat-container .message.system .message-text {\r\n color: #ffa500 !important;\r\n}\r\n\r\n#app-chat-container .message.sent {\r\n background: #00ff0020 !important;\r\n border: 1px solid #00ff0030 !important;\r\n border-left: 3px solid #00ff00 !important;\r\n}\r\n\r\n#app-chat-container .message.sent .time {\r\n color: #00ff0060 !important;\r\n}\r\n\r\n#app-chat-container .message.sent .username,\r\n#app-chat-container .message.sent .message-text {\r\n color: #00ff00 !important;\r\n}\r\n\r\n#app-chat-container .message.received {\r\n background-color: #ff4d4d20 !important;\r\n border: 1px solid #ff4d4d30 !important;\r\n border-left: 3px solid #ff4d4d !important;\r\n}\r\n\r\n#app-chat-container .message.received .time {\r\n color: #ff4d4d60 !important;\r\n}\r\n\r\n#app-chat-container .message.received .username,\r\n#app-chat-container .message.received .message-text {\r\n color: #ff4d4d !important;\r\n}\r\n\r\n/* Private message styling */\r\n.message.private-message {\r\n background-color: #ffdcdc20;\r\n border-left: 3px solid #ff6b6b50;\r\n}\r\n\r\n.message.private-message .message-text {\r\n color: #ff6b6b !important;\r\n}\r\n\r\n.message.private-message:not(.sent) {\r\n animation: privateMessagePulse 2s ease-in-out 1;\r\n}\r\n\r\n/* Sent private message styling */\r\n.message.private-message.sent {\r\n background: #293e2938 !important;\r\n border: 1px solid #293e2959 !important;\r\n}\r\n\r\n/* Received private message styling */\r\n.message.private-message.received {\r\n background-color: #1e1e1e26 !important;\r\n border: 1px solid #33333359 !important;\r\n}\r\n\r\n/* Add a subtle animation for received private messages */\r\n@keyframes privateMessagePulse {\r\n 0% {\r\n background-color: #ffdcdc26;\r\n }\r\n\r\n 50% {\r\n background-color: #ffdcdc4d;\r\n }\r\n\r\n 100% {\r\n background-color: #ffdcdc26;\r\n }\r\n}\r\n\r\n#app-chat-container .message-info {\r\n margin-right: 1em !important;\r\n white-space: nowrap !important;\r\n}\r\n\r\n/* Ensure time and username elements maintain their relative sizes */\r\n#app-chat-container .message-info .time {\r\n font-size: 0.9em !important;\r\n margin-right: 1em !important;\r\n color: #666 !important;\r\n}\r\n\r\n#app-chat-container .username {\r\n font-size: 1em !important;\r\n}\r\n\r\n/* Add to existing style.css content */\r\n#app-chat-container .username,\r\n#app-chat-container .time {\r\n cursor: pointer !important;\r\n transition: opacity 0.2s ease !important;\r\n}\r\n\r\n#app-chat-container .username:hover,\r\n#app-chat-container .time:hover {\r\n opacity: 0.7 !important;\r\n}\r\n\r\n#app-chat-container .message-info .time {\r\n margin-right: 1em !important;\r\n font-size: 0.9em !important;\r\n color: #666 !important;\r\n}\r\n\r\n/* User list item styles */\r\n#app-chat-container .user-item {\r\n display: flex !important;\r\n align-items: center !important;\r\n padding: 0.2em !important;\r\n margin-bottom: 0.2em !important;\r\n border-radius: var(--border-radius) !important;\r\n max-width: 100% !important;\r\n text-overflow: ellipsis !important;\r\n}\r\n\r\n#app-chat-container .user-avatar {\r\n display: flex !important;\r\n justify-content: center !important;\r\n align-items: center !important;\r\n width: 24px !important;\r\n height: 24px !important;\r\n font-size: 18px !important;\r\n border-radius: 0.1em !important;\r\n margin-right: 1em !important;\r\n text-align: center !important;\r\n line-height: 24px !important;\r\n flex-shrink: 0 !important;\r\n}\r\n\r\n#app-chat-container .user-avatar.image-avatar {\r\n cursor: pointer !important;\r\n transform-origin: left !important;\r\n transition: transform 0.15s ease-out !important;\r\n}\r\n\r\n#app-chat-container .user-avatar.image-avatar:hover {\r\n transform: scale(2) !important;\r\n}\r\n\r\n#app-chat-container .user-avatar.svg-avatar {\r\n font-family: var(--emoji-font), sans-serif !important;\r\n}\r\n\r\n#app-chat-container .user-info {\r\n flex: 1 !important;\r\n min-width: 0 !important;\r\n overflow: hidden !important;\r\n text-overflow: ellipsis !important;\r\n white-space: nowrap !important;\r\n}\r\n\r\n#app-chat-container .user-meta {\r\n cursor: default;\r\n font-size: 0.8em !important;\r\n color: #b0b0b0 !important;\r\n overflow: hidden !important;\r\n text-overflow: ellipsis !important;\r\n white-space: nowrap !important;\r\n}\r\n\r\n#app-chat-container .game-link {\r\n color: #deb887 !important;\r\n text-decoration: none !important;\r\n transition: color 0.15s !important;\r\n}\r\n\r\n#app-chat-container .role-moderator {\r\n color: #ff7e7e !important;\r\n}\r\n\r\n#app-chat-container .role-participant {\r\n color: #7ed4ff !important;\r\n}\r\n\r\n#app-chat-container .role-visitor {\r\n color: #b0b0b0 !important;\r\n}\r\n\r\n#app-chat-container .role {\r\n font-family: var(--emoji-font), sans-serif !important;\r\n}\r\n\r\n#app-chat-container .role.participant {\r\n filter: brightness(0.6) !important;\r\n}\r\n\r\n#app-chat-container .role.visitor {\r\n filter: brightness(0.8) !important;\r\n}\r\n\r\n#app-chat-container .traffic-icon {\r\n /* Set font-family to var(--emoji-font) or fallback to system emoji */\r\n font-family: var(--emoji-font), sans-serif !important;\r\n}\r\n\r\n/* Header toggle button */\r\n#app-chat-container .header-button {\r\n cursor: pointer !important;\r\n position: absolute !important;\r\n top: 0 !important;\r\n width: 25px !important;\r\n height: 25px !important;\r\n z-index: 5 !important;\r\n}\r\n\r\n#app-chat-container .filled-button {\r\n border: none !important;\r\n outline: none !important;\r\n background-color: transparent !important;\r\n transition: all 0.15s ease-out;\r\n}\r\n\r\n#app-chat-container .filled-button:hover {\r\n filter: brightness(1.2) !important;\r\n}\r\n\r\n#app-chat-container .emoji-trigger,\r\n#app-chat-container .private-mode-exit,\r\n#app-chat-container .send-button {\r\n font-family: var(--emoji-font), sans-serif !important;\r\n height: 28px !important;\r\n width: 28px !important;\r\n font-size: 1.5em !important;\r\n display: flex !important;\r\n align-items: center !important;\r\n justify-content: center !important;\r\n line-height: 1 !important;\r\n cursor: pointer !important;\r\n background: transparent !important;\r\n border: none !important;\r\n outline: none !important;\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n}\r\n\r\n.chat-toggle-button {\r\n right: 0 !important;\r\n}\r\n\r\n.chat-maximize-button {\r\n right: 25px !important;\r\n}\r\n\r\n.chat-help-button {\r\n color: #82B32A !important;\r\n right: 50px !important;\r\n}\r\n\r\n/* Drag area for floating the chat */\r\n#app-chat-container .chat-drag-area {\r\n border-radius: 0.4em 0.4em 0 0 !important;\r\n position: absolute !important;\r\n top: 0 !important;\r\n left: 0 !important;\r\n right: 0 !important;\r\n height: 25px !important;\r\n cursor: move !important;\r\n background-color: #161616cc !important;\r\n}\r\n\r\n/* Dimming background style */\r\n.dimming-element {\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n background-color: #00000080;\r\n z-index: 1010 !important;\r\n opacity: 0;\r\n transition: opacity 0.3s ease;\r\n}\r\n\r\n/* convertImageLinksToImage */\r\n.clickable-thumbnail {\r\n opacity: 1;\r\n transition: opacity 0.15s ease-in-out;\r\n border: none;\r\n max-width: 150px !important;\r\n max-height: 150px !important;\r\n cursor: pointer;\r\n background-color: transparent;\r\n padding: 2px;\r\n margin: 6px;\r\n overflow-y: auto;\r\n}\r\n\r\n.clickable-thumbnail img {\r\n border-radius: var(--border-radius) !important;\r\n object-fit: contain;\r\n height: 100%;\r\n width: 100%;\r\n}\r\n\r\n.clickable-thumbnail:hover {\r\n opacity: 0.8;\r\n}\r\n\r\n.scaled-thumbnail {\r\n top: 50%;\r\n left: 50%;\r\n transform-origin: center center;\r\n transform: translate(-50%, -50%) scale(1);\r\n position: fixed;\r\n opacity: 0;\r\n z-index: 1015 !important;\r\n transform-origin: center center;\r\n max-height: 90vh;\r\n max-width: 90vw;\r\n cursor: pointer;\r\n border-radius: 0.6em !important;\r\n box-shadow: 0 4px 6px #0000004d, 0 1px 3px #00000047 !important;\r\n}\r\n\r\n/* convertVideoLinksToPlayer */\r\n.video-wrapper {\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.video-wrapper .processed-video {\r\n margin-bottom: 0.6em !important;\r\n}\r\n\r\n.video-container {\r\n border-radius: 0.4em !important;\r\n display: flex;\r\n border: none;\r\n height: 200px !important;\r\n width: 356px !important;\r\n}`, \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/styles/style.css?./node_modules/css-loader/dist/cjs.js");
/***/ }),
/***/ "./node_modules/css-loader/dist/runtime/api.js":
/*!*****************************************************!*\
!*** ./node_modules/css-loader/dist/runtime/api.js ***!
\*****************************************************/
/***/ ((module) => {
eval("\r\n\r\n/*\r\n MIT License http://www.opensource.org/licenses/mit-license.php\r\n Author Tobias Koppers @sokra\r\n*/\r\nmodule.exports = function (cssWithMappingToString) {\r\n var list = [];\r\n\r\n // return the list of modules as css string\r\n list.toString = function toString() {\r\n return this.map(function (item) {\r\n var content = \"\";\r\n var needLayer = typeof item[5] !== \"undefined\";\r\n if (item[4]) {\r\n content += \"@supports (\".concat(item[4], \") {\");\r\n }\r\n if (item[2]) {\r\n content += \"@media \".concat(item[2], \" {\");\r\n }\r\n if (needLayer) {\r\n content += \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\");\r\n }\r\n content += cssWithMappingToString(item);\r\n if (needLayer) {\r\n content += \"}\";\r\n }\r\n if (item[2]) {\r\n content += \"}\";\r\n }\r\n if (item[4]) {\r\n content += \"}\";\r\n }\r\n return content;\r\n }).join(\"\");\r\n };\r\n\r\n // import a list of modules into the list\r\n list.i = function i(modules, media, dedupe, supports, layer) {\r\n if (typeof modules === \"string\") {\r\n modules = [[null, modules, undefined]];\r\n }\r\n var alreadyImportedModules = {};\r\n if (dedupe) {\r\n for (var k = 0; k < this.length; k++) {\r\n var id = this[k][0];\r\n if (id != null) {\r\n alreadyImportedModules[id] = true;\r\n }\r\n }\r\n }\r\n for (var _k = 0; _k < modules.length; _k++) {\r\n var item = [].concat(modules[_k]);\r\n if (dedupe && alreadyImportedModules[item[0]]) {\r\n continue;\r\n }\r\n if (typeof layer !== \"undefined\") {\r\n if (typeof item[5] === \"undefined\") {\r\n item[5] = layer;\r\n } else {\r\n item[1] = \"@layer\".concat(item[5].length > 0 ? \" \".concat(item[5]) : \"\", \" {\").concat(item[1], \"}\");\r\n item[5] = layer;\r\n }\r\n }\r\n if (media) {\r\n if (!item[2]) {\r\n item[2] = media;\r\n } else {\r\n item[1] = \"@media \".concat(item[2], \" {\").concat(item[1], \"}\");\r\n item[2] = media;\r\n }\r\n }\r\n if (supports) {\r\n if (!item[4]) {\r\n item[4] = \"\".concat(supports);\r\n } else {\r\n item[1] = \"@supports (\".concat(item[4], \") {\").concat(item[1], \"}\");\r\n item[4] = supports;\r\n }\r\n }\r\n list.push(item);\r\n }\r\n };\r\n return list;\r\n};\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/css-loader/dist/runtime/api.js?");
/***/ }),
/***/ "./node_modules/css-loader/dist/runtime/noSourceMaps.js":
/*!**************************************************************!*\
!*** ./node_modules/css-loader/dist/runtime/noSourceMaps.js ***!
\**************************************************************/
/***/ ((module) => {
eval("\r\n\r\nmodule.exports = function (i) {\r\n return i[1];\r\n};\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/css-loader/dist/runtime/noSourceMaps.js?");
/***/ }),
/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
/*!****************************************************************************!*\
!*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***!
\****************************************************************************/
/***/ ((module) => {
eval("\r\n\r\nvar stylesInDOM = [];\r\nfunction getIndexByIdentifier(identifier) {\r\n var result = -1;\r\n for (var i = 0; i < stylesInDOM.length; i++) {\r\n if (stylesInDOM[i].identifier === identifier) {\r\n result = i;\r\n break;\r\n }\r\n }\r\n return result;\r\n}\r\nfunction modulesToDom(list, options) {\r\n var idCountMap = {};\r\n var identifiers = [];\r\n for (var i = 0; i < list.length; i++) {\r\n var item = list[i];\r\n var id = options.base ? item[0] + options.base : item[0];\r\n var count = idCountMap[id] || 0;\r\n var identifier = \"\".concat(id, \" \").concat(count);\r\n idCountMap[id] = count + 1;\r\n var indexByIdentifier = getIndexByIdentifier(identifier);\r\n var obj = {\r\n css: item[1],\r\n media: item[2],\r\n sourceMap: item[3],\r\n supports: item[4],\r\n layer: item[5]\r\n };\r\n if (indexByIdentifier !== -1) {\r\n stylesInDOM[indexByIdentifier].references++;\r\n stylesInDOM[indexByIdentifier].updater(obj);\r\n } else {\r\n var updater = addElementStyle(obj, options);\r\n options.byIndex = i;\r\n stylesInDOM.splice(i, 0, {\r\n identifier: identifier,\r\n updater: updater,\r\n references: 1\r\n });\r\n }\r\n identifiers.push(identifier);\r\n }\r\n return identifiers;\r\n}\r\nfunction addElementStyle(obj, options) {\r\n var api = options.domAPI(options);\r\n api.update(obj);\r\n var updater = function updater(newObj) {\r\n if (newObj) {\r\n if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap && newObj.supports === obj.supports && newObj.layer === obj.layer) {\r\n return;\r\n }\r\n api.update(obj = newObj);\r\n } else {\r\n api.remove();\r\n }\r\n };\r\n return updater;\r\n}\r\nmodule.exports = function (list, options) {\r\n options = options || {};\r\n list = list || [];\r\n var lastIdentifiers = modulesToDom(list, options);\r\n return function update(newList) {\r\n newList = newList || [];\r\n for (var i = 0; i < lastIdentifiers.length; i++) {\r\n var identifier = lastIdentifiers[i];\r\n var index = getIndexByIdentifier(identifier);\r\n stylesInDOM[index].references--;\r\n }\r\n var newLastIdentifiers = modulesToDom(newList, options);\r\n for (var _i = 0; _i < lastIdentifiers.length; _i++) {\r\n var _identifier = lastIdentifiers[_i];\r\n var _index = getIndexByIdentifier(_identifier);\r\n if (stylesInDOM[_index].references === 0) {\r\n stylesInDOM[_index].updater();\r\n stylesInDOM.splice(_index, 1);\r\n }\r\n }\r\n lastIdentifiers = newLastIdentifiers;\r\n };\r\n};\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js?");
/***/ }),
/***/ "./node_modules/style-loader/dist/runtime/insertBySelector.js":
/*!********************************************************************!*\
!*** ./node_modules/style-loader/dist/runtime/insertBySelector.js ***!
\********************************************************************/
/***/ ((module) => {
eval("\r\n\r\nvar memo = {};\r\n\r\n/* istanbul ignore next */\r\nfunction getTarget(target) {\r\n if (typeof memo[target] === \"undefined\") {\r\n var styleTarget = document.querySelector(target);\r\n\r\n // Special case to return head of iframe instead of iframe itself\r\n if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {\r\n try {\r\n // This will throw an exception if access to iframe is blocked\r\n // due to cross-origin restrictions\r\n styleTarget = styleTarget.contentDocument.head;\r\n } catch (e) {\r\n // istanbul ignore next\r\n styleTarget = null;\r\n }\r\n }\r\n memo[target] = styleTarget;\r\n }\r\n return memo[target];\r\n}\r\n\r\n/* istanbul ignore next */\r\nfunction insertBySelector(insert, style) {\r\n var target = getTarget(insert);\r\n if (!target) {\r\n throw new Error(\"Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.\");\r\n }\r\n target.appendChild(style);\r\n}\r\nmodule.exports = insertBySelector;\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/style-loader/dist/runtime/insertBySelector.js?");
/***/ }),
/***/ "./node_modules/style-loader/dist/runtime/insertStyleElement.js":
/*!**********************************************************************!*\
!*** ./node_modules/style-loader/dist/runtime/insertStyleElement.js ***!
\**********************************************************************/
/***/ ((module) => {
eval("\r\n\r\n/* istanbul ignore next */\r\nfunction insertStyleElement(options) {\r\n var element = document.createElement(\"style\");\r\n options.setAttributes(element, options.attributes);\r\n options.insert(element, options.options);\r\n return element;\r\n}\r\nmodule.exports = insertStyleElement;\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/style-loader/dist/runtime/insertStyleElement.js?");
/***/ }),
/***/ "./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js":
/*!**********************************************************************************!*\
!*** ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js ***!
\**********************************************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
eval("\r\n\r\n/* istanbul ignore next */\r\nfunction setAttributesWithoutAttributes(styleElement) {\r\n var nonce = true ? __webpack_require__.nc : 0;\r\n if (nonce) {\r\n styleElement.setAttribute(\"nonce\", nonce);\r\n }\r\n}\r\nmodule.exports = setAttributesWithoutAttributes;\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js?");
/***/ }),
/***/ "./node_modules/style-loader/dist/runtime/styleDomAPI.js":
/*!***************************************************************!*\
!*** ./node_modules/style-loader/dist/runtime/styleDomAPI.js ***!
\***************************************************************/
/***/ ((module) => {
eval("\r\n\r\n/* istanbul ignore next */\r\nfunction apply(styleElement, options, obj) {\r\n var css = \"\";\r\n if (obj.supports) {\r\n css += \"@supports (\".concat(obj.supports, \") {\");\r\n }\r\n if (obj.media) {\r\n css += \"@media \".concat(obj.media, \" {\");\r\n }\r\n var needLayer = typeof obj.layer !== \"undefined\";\r\n if (needLayer) {\r\n css += \"@layer\".concat(obj.layer.length > 0 ? \" \".concat(obj.layer) : \"\", \" {\");\r\n }\r\n css += obj.css;\r\n if (needLayer) {\r\n css += \"}\";\r\n }\r\n if (obj.media) {\r\n css += \"}\";\r\n }\r\n if (obj.supports) {\r\n css += \"}\";\r\n }\r\n var sourceMap = obj.sourceMap;\r\n if (sourceMap && typeof btoa !== \"undefined\") {\r\n css += \"\\n/*# sourceMappingURL=data:application/json;base64,\".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), \" */\");\r\n }\r\n\r\n // For old IE\r\n /* istanbul ignore if */\r\n options.styleTagTransform(css, styleElement, options.options);\r\n}\r\nfunction removeStyleElement(styleElement) {\r\n // istanbul ignore if\r\n if (styleElement.parentNode === null) {\r\n return false;\r\n }\r\n styleElement.parentNode.removeChild(styleElement);\r\n}\r\n\r\n/* istanbul ignore next */\r\nfunction domAPI(options) {\r\n if (typeof document === \"undefined\") {\r\n return {\r\n update: function update() {},\r\n remove: function remove() {}\r\n };\r\n }\r\n var styleElement = options.insertStyleElement(options);\r\n return {\r\n update: function update(obj) {\r\n apply(styleElement, options, obj);\r\n },\r\n remove: function remove() {\r\n removeStyleElement(styleElement);\r\n }\r\n };\r\n}\r\nmodule.exports = domAPI;\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/style-loader/dist/runtime/styleDomAPI.js?");
/***/ }),
/***/ "./node_modules/style-loader/dist/runtime/styleTagTransform.js":
/*!*********************************************************************!*\
!*** ./node_modules/style-loader/dist/runtime/styleTagTransform.js ***!
\*********************************************************************/
/***/ ((module) => {
eval("\r\n\r\n/* istanbul ignore next */\r\nfunction styleTagTransform(css, styleElement) {\r\n if (styleElement.styleSheet) {\r\n styleElement.styleSheet.cssText = css;\r\n } else {\r\n while (styleElement.firstChild) {\r\n styleElement.removeChild(styleElement.firstChild);\r\n }\r\n styleElement.appendChild(document.createTextNode(css));\r\n }\r\n}\r\nmodule.exports = styleTagTransform;\n\n//# sourceURL=webpack://tampermonkey-script/./node_modules/style-loader/dist/runtime/styleTagTransform.js?");
/***/ }),
/***/ "./src/animations.js":
/*!***************************!*\
!*** ./src/animations.js ***!
\***************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ addJumpEffect: () => (/* binding */ addJumpEffect),\n/* harmony export */ addPulseEffect: () => (/* binding */ addPulseEffect),\n/* harmony export */ addShakeEffect: () => (/* binding */ addShakeEffect)\n/* harmony export */ });\nfunction addJumpEffect(element, initialTranslateX = 0, initialTranslateY = 0) {\r\n // Define keyframes with specified percentages, scale effect, and calc for Y translation\r\n const keyframes = [\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}%)) scale(1)` }, // 0%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}% - 60%)) scale(1.1)` }, // 20%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}% + 15%)) scale(1)` }, // 40%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}% - 20%)) scale(1.05)` }, // 60%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}% + 8%)) scale(1)` }, // 75%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}% - 10%)) scale(1.05)` }, // 85%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}% + 4%)) scale(1)` }, // 92%\r\n { transform: `translate(${initialTranslateX}%, calc(${initialTranslateY}%)) scale(1)` } // 100%\r\n ];\r\n\r\n // Animation options\r\n const options = {\r\n duration: 500, // Total animation duration in ms (adjust as needed)\r\n easing: 'ease', // Smooth easing between keyframes\r\n iterations: 1 // Play once\r\n };\r\n\r\n // Start the animation\r\n const animation = element.animate(keyframes, options);\r\n\r\n // Optional: Return a promise that resolves when animation completes\r\n return animation.finished;\r\n}\r\n\r\n// Helper function to add shake effect\r\nfunction addShakeEffect(element) {\r\n element.classList.add('shake-effect');\r\n setTimeout(() => {\r\n element.classList.remove('shake-effect');\r\n }, 500);\r\n}\r\n\r\nfunction addPulseEffect(element) {\r\n element.classList.add('pulse-effect');\r\n setTimeout(() => {\r\n element.classList.remove('pulse-effect');\r\n }, 500);\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/animations.js?");
/***/ }),
/***/ "./src/auth.js":
/*!*********************!*\
!*** ./src/auth.js ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ getAuthData: () => (/* binding */ getAuthData)\n/* harmony export */ });\nfunction getAuthData() {\r\n // Only proceed if on the gamelist page\r\n if (!window.location.href.startsWith('https://klavogonki.ru/gamelist/')) return;\r\n\r\n try {\r\n // Find the script containing PageData\r\n const script = Array.from(document.scripts).find(s => s.text.includes('PageData'));\r\n if (!script) throw new Error('PageData script not found');\r\n\r\n // Extract and parse the JSON-like data inside the script\r\n const rawData = script.text.match(/\\.constant\\('PageData', ([\\s\\S]*?})\\)/)[1];\r\n const parsedData = JSON.parse(\r\n rawData\r\n .replace(/(\\w+):/g, '\"$1\":') // Fix object keys\r\n .replace(/'/g, '\"') // Fix string quotes\r\n );\r\n\r\n const username = `${parsedData.chatParams.user.id}#${parsedData.chatParams.user.login}`;\r\n const password = parsedData.chatParams.pass;\r\n\r\n // Redirect only if it hasn’t happened before\r\n if (!localStorage.getItem('klavoauth')) {\r\n // Always update klavoauth with the latest data\r\n localStorage.setItem('klavoauth', JSON.stringify({ username, password }));\r\n setTimeout(() => {\r\n window.location.href = 'https://klavogonki.ru';\r\n }, 500);\r\n }\r\n } catch (e) {\r\n console.error('Auth error:', e);\r\n localStorage.removeItem('klavoauth');\r\n // Optional: Reset redirect flag to allow retry after error\r\n // localStorage.removeItem('klavoauth_redirected');\r\n alert(`Auth failed: ${e.message}\\nPlease refresh the page.`);\r\n }\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/auth.js?");
/***/ }),
/***/ "./src/chatFeatures.js":
/*!*****************************!*\
!*** ./src/chatFeatures.js ***!
\*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ addChatToggleFeature: () => (/* binding */ addChatToggleFeature),\n/* harmony export */ toggleChatVisibility: () => (/* binding */ toggleChatVisibility)\n/* harmony export */ });\n/* harmony import */ var _icons_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./icons.js */ \"./src/icons.js\");\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./helpers.js */ \"./src/helpers.js\");\n/* harmony import */ var _chatUI_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./chatUI.js */ \"./src/chatUI.js\");\n\r\n\r\n\r\n\r\nfunction toggleChatVisibility() {\r\n const chatContainer = document.getElementById('app-chat-container');\r\n const toggleButton = document.querySelector('.chat-toggle-button');\r\n if (!chatContainer) return;\r\n\r\n // Prevent toggling visibility if chat is maximized\r\n if (chatContainer.classList.contains('maximized')) {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.showChatAlert)('Chat is currently maximized', {\r\n type: 'warning',\r\n duration: 1000\r\n });\r\n return;\r\n }\r\n\r\n const chatState = JSON.parse(localStorage.getItem('chatState')) || {};\r\n const isFloating = chatState.floating || false;\r\n\r\n if (isFloating) {\r\n const isBecomingVisible = chatContainer.style.opacity === '0';\r\n chatContainer.style.opacity = isBecomingVisible ? '1' : '0';\r\n setTimeout(() => {\r\n chatContainer.style.display = isBecomingVisible ? 'flex' : 'none';\r\n toggleButton.innerHTML = isBecomingVisible ? _icons_js__WEBPACK_IMPORTED_MODULE_0__.closeSVG : _icons_js__WEBPACK_IMPORTED_MODULE_0__.openSVG;\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.saveChatState)({\r\n ...chatState,\r\n isVisible: isBecomingVisible\r\n });\r\n if (isBecomingVisible) {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.focusTextInput)(); // Focus input after chat becomes visible\r\n }\r\n }, 300);\r\n } else {\r\n const isCurrentlyVisible = chatContainer.classList.contains('visible-chat');\r\n const isBecomingVisible = !isCurrentlyVisible;\r\n chatContainer.classList.remove('visible-chat', 'hidden-chat');\r\n chatContainer.classList.add(isBecomingVisible ? 'visible-chat' : 'hidden-chat');\r\n toggleButton.innerHTML = isBecomingVisible ? _icons_js__WEBPACK_IMPORTED_MODULE_0__.closeSVG : _icons_js__WEBPACK_IMPORTED_MODULE_0__.openSVG;\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.saveChatState)({\r\n ...chatState,\r\n isVisible: isBecomingVisible\r\n });\r\n if (isBecomingVisible) {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.focusTextInput)(); // Focus input immediately when shown\r\n }\r\n }\r\n}\r\n\r\nfunction addChatToggleFeature() {\r\n const chatContainer = document.getElementById('app-chat-container');\r\n const closeButton = document.getElementById('chat-close-btn');\r\n const draggableHeader = document.getElementById('chat-header');\r\n if (!chatContainer) return;\r\n\r\n // Restore initial visibility from saved state\r\n const chatState = JSON.parse(localStorage.getItem('chatState')) || {};\r\n const isFloating = chatState.floating || false;\r\n const isVisible = chatState.isVisible !== false;\r\n\r\n if (isFloating) {\r\n chatContainer.style.display = isVisible ? 'flex' : 'none';\r\n chatContainer.style.opacity = isVisible ? '1' : '0';\r\n } else {\r\n chatContainer.classList.remove('visible-chat', 'hidden-chat');\r\n chatContainer.classList.add(isVisible ? 'visible-chat' : 'hidden-chat');\r\n }\r\n\r\n // Keyboard shortcuts\r\n document.addEventListener('keydown', (e) => {\r\n if (e.ctrlKey && e.shiftKey && e.code === 'Space') {\r\n e.preventDefault();\r\n (0,_chatUI_js__WEBPACK_IMPORTED_MODULE_2__.toggleChatMaximize)();\r\n } else if (e.ctrlKey && e.code === 'Space') {\r\n e.preventDefault();\r\n toggleChatVisibility();\r\n }\r\n });\r\n\r\n if (closeButton) {\r\n closeButton.addEventListener('click', toggleChatVisibility);\r\n }\r\n\r\n if (draggableHeader) {\r\n draggableHeader.addEventListener('dblclick', toggleChatVisibility);\r\n }\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/chatFeatures.js?");
/***/ }),
/***/ "./src/chatUI.js":
/*!***********************!*\
!*** ./src/chatUI.js ***!
\***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ createChatUI: () => (/* binding */ createChatUI),\n/* harmony export */ toggleChatMaximize: () => (/* binding */ toggleChatMaximize)\n/* harmony export */ });\n/* harmony import */ var _chatFeatures_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./chatFeatures.js */ \"./src/chatFeatures.js\");\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./helpers.js */ \"./src/helpers.js\");\n/* harmony import */ var _icons_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./icons.js */ \"./src/icons.js\");\n/* harmony import */ var _components_helpPanel_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components/helpPanel.js */ \"./src/components/helpPanel.js\");\n/* harmony import */ var _components_emojiPanel_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components/emojiPanel.js */ \"./src/components/emojiPanel.js\");\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\nfunction createChatUI() {\r\n const chatContainer = document.createElement('div');\r\n chatContainer.id = 'app-chat-container';\r\n // Add resize handles\r\n ['top', 'left', 'right'].forEach(type => {\r\n const handle = document.createElement('div');\r\n handle.className = `resize-handle ${type}`;\r\n chatContainer.appendChild(handle);\r\n });\r\n // Chat wrapper for content and user list\r\n const chatWrapper = document.createElement('div');\r\n chatWrapper.className = 'chat-wrapper';\r\n // Left side: messages panel and input\r\n const chatContent = document.createElement('div');\r\n chatContent.className = 'chat-content';\r\n const messagesPanel = document.createElement('div');\r\n messagesPanel.id = 'messages-panel';\r\n messagesPanel.className = 'messages-panel';\r\n const inputContainer = document.createElement('div');\r\n inputContainer.className = 'input-container';\r\n // Create emoji button\r\n const emojiButton = document.createElement('button');\r\n emojiButton.className = 'emoji-trigger filled-button';\r\n emojiButton.innerHTML = \"🙂\";\r\n emojiButton.classList.add('emoji-button');\r\n emojiButton.title = 'Open emoji picker';\r\n\r\n // Add these event listeners to change the emoji on hover\r\n emojiButton.addEventListener('mouseover', () => {\r\n emojiButton.innerHTML = \"🙃\";\r\n });\r\n\r\n emojiButton.addEventListener('mouseout', () => {\r\n emojiButton.innerHTML = \"🙂\";\r\n });\r\n\r\n // Setup random emoji appearance with range (10min - 30min)\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.setupRandomEmojiAttention)(emojiButton, (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.getRandomInterval)(600000, 1800000));\r\n\r\n // Setup emoji panel toggle functionality\r\n let emojiPanelInstance = null;\r\n emojiButton.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (!emojiPanelInstance || !document.querySelector('.emoji-panel')) {\r\n emojiPanelInstance = new _components_emojiPanel_js__WEBPACK_IMPORTED_MODULE_4__.EmojiPanel({\r\n container: messagesPanel,\r\n position: 'bottom',\r\n emojiButton: emojiButton,\r\n onEmojiSelect: (emoji) => {\r\n const messageInput = document.getElementById('message-input');\r\n if (messageInput) {\r\n const cursorPos = messageInput.selectionStart;\r\n const textBefore = messageInput.value.substring(0, cursorPos);\r\n const textAfter = messageInput.value.substring(messageInput.selectionEnd);\r\n messageInput.value = textBefore + emoji + textAfter;\r\n const newCursorPos = cursorPos + emoji.length;\r\n messageInput.setSelectionRange(newCursorPos, newCursorPos);\r\n messageInput.focus();\r\n }\r\n },\r\n onDestroy: () => {\r\n emojiButton.title = 'Open emoji picker';\r\n emojiPanelInstance = null;\r\n }\r\n });\r\n emojiPanelInstance.init();\r\n emojiButton.title = 'Close emoji picker';\r\n } else {\r\n emojiPanelInstance.destroy();\r\n }\r\n });\r\n // Create message input\r\n const messageInput = document.createElement('input');\r\n messageInput.type = 'text';\r\n messageInput.id = 'message-input';\r\n messageInput.maxLength = 300;\r\n // Create send button\r\n const sendButton = document.createElement('button');\r\n sendButton.id = 'send-button';\r\n sendButton.className = 'filled-button send-button';\r\n sendButton.innerHTML = _icons_js__WEBPACK_IMPORTED_MODULE_2__.sendSVG;\r\n // Append elements in order\r\n inputContainer.appendChild(emojiButton);\r\n inputContainer.appendChild(messageInput);\r\n inputContainer.appendChild(sendButton);\r\n chatContent.appendChild(messagesPanel);\r\n chatContent.appendChild(inputContainer);\r\n // Right side: user list\r\n const userListContainer = document.createElement('div');\r\n userListContainer.className = 'user-list-container';\r\n const userList = document.createElement('div');\r\n userList.id = 'user-list';\r\n userListContainer.appendChild(userList);\r\n chatWrapper.appendChild(chatContent);\r\n chatWrapper.appendChild(userListContainer);\r\n chatContainer.appendChild(chatWrapper);\r\n // Maximize button\r\n const maximizeButton = document.createElement('button');\r\n maximizeButton.className = 'filled-button header-button chat-maximize-button';\r\n maximizeButton.innerHTML = _icons_js__WEBPACK_IMPORTED_MODULE_2__.expandSVG;\r\n maximizeButton.addEventListener('click', toggleChatMaximize);\r\n chatContainer.appendChild(maximizeButton);\r\n // Help button next to maximize button\r\n const helpButton = document.createElement('button');\r\n helpButton.className = 'filled-button header-button chat-help-button';\r\n helpButton.innerHTML = _icons_js__WEBPACK_IMPORTED_MODULE_2__.helpSVG; // Replace with desired icon if available\r\n helpButton.title = 'Show chat help';\r\n // Declare a variable to track the help panel instance.\r\n let helpPanelInstance = null;\r\n\r\n helpButton.addEventListener('click', (e) => {\r\n console.log(\"Help button clicked.\");\r\n e.stopPropagation();\r\n\r\n // If a help panel exists, remove it and exit.\r\n if (helpPanelInstance && document.querySelector('.help-panel')) {\r\n helpPanelInstance.remove();\r\n helpButton.title = 'Show chat help';\r\n helpPanelInstance = null;\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.showChatAlert)('Help panel has been closed.', {\r\n type: 'warning',\r\n duration: 2000\r\n });\r\n return;\r\n }\r\n\r\n // Otherwise, create a new help panel.\r\n console.log(\"Help panel does not exist. Creating help panel...\");\r\n helpPanelInstance = new _components_helpPanel_js__WEBPACK_IMPORTED_MODULE_3__.HelpPanel({\r\n helpButton: helpButton,\r\n onDestroy: () => {\r\n helpButton.title = 'Show chat help';\r\n helpPanelInstance = null;\r\n }\r\n });\r\n helpPanelInstance.init();\r\n helpPanelInstance.show();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.showChatAlert)('Help panel has been opened. Press \"?\" or \"ESC\" key, or click outside to close.', {\r\n type: 'success',\r\n duration: 2000\r\n });\r\n helpButton.title = 'Hide chat help';\r\n });\r\n\r\n chatContainer.appendChild(helpButton);\r\n // Toggle visibility button\r\n const toggleButton = document.createElement('button');\r\n toggleButton.className = 'filled-button header-button chat-toggle-button';\r\n toggleButton.innerHTML = _icons_js__WEBPACK_IMPORTED_MODULE_2__.closeSVG;\r\n toggleButton.addEventListener('click', _chatFeatures_js__WEBPACK_IMPORTED_MODULE_0__.toggleChatVisibility);\r\n chatContainer.appendChild(toggleButton);\r\n // Draggable top area\r\n const topArea = document.createElement('div');\r\n topArea.className = 'chat-drag-area';\r\n topArea.addEventListener('dblclick', _chatFeatures_js__WEBPACK_IMPORTED_MODULE_0__.toggleChatVisibility);\r\n chatContainer.appendChild(topArea);\r\n document.body.appendChild(chatContainer);\r\n // Restore chat state and settings\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.restoreChatState)();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.createFontSizeControl)();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.restoreFontSize)();\r\n // Force scroll to bottom once after chat creation\r\n requestAnimationFrame(() => {\r\n const messagesPanel = document.getElementById('messages-panel');\r\n const messageInput = document.getElementById('message-input');\r\n if (messagesPanel && messageInput) {\r\n messagesPanel.scrollTop = messagesPanel.scrollHeight;\r\n // Pass the input element and messages panel into the helper functions.\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.createLengthPopup)(messagesPanel);\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.initChatLengthPopupEvents)(messageInput);\r\n }\r\n });\r\n}\r\n\r\nlet originalChatState = null;\r\n\r\nfunction toggleChatMaximize() {\r\n const chat = document.getElementById('app-chat-container');\r\n const maximizeButton = document.querySelector('.chat-maximize-button');\r\n if (!chat) return;\r\n if (!chat.classList.contains('maximized')) {\r\n const hasVisibilityClass = !chat.classList.contains('visible-chat') && !chat.classList.contains('hidden-chat');\r\n originalChatState = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.getChatState)();\r\n const calculateHeight = () => `${Math.floor(window.innerHeight * 0.9)}px`;\r\n chat.style.cssText = `\r\n width: 100vw !important;\r\n height: ${calculateHeight()} !important;\r\n max-width: 100vw !important;\r\n min-width: 100vw !important;\r\n position: fixed !important;\r\n bottom: 0 !important;\r\n left: 0 !important;\r\n right: 0 !important;\r\n top: auto !important;\r\n margin: 0 !important;\r\n transform: none !important;\r\n `;\r\n if (hasVisibilityClass) {\r\n chat.classList.remove('visible-chat', 'hidden-chat');\r\n }\r\n chat.classList.add('maximized');\r\n maximizeButton.classList.add('maximized');\r\n maximizeButton.innerHTML = _icons_js__WEBPACK_IMPORTED_MODULE_2__.collapseSVG;\r\n const resizeHandler = () => {\r\n chat.style.height = calculateHeight();\r\n chat.style.bottom = '0';\r\n chat.style.top = 'auto';\r\n };\r\n window.addEventListener('resize', resizeHandler);\r\n chat.maximizeResizeHandler = resizeHandler;\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.handleElementsBehavior)();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.focusTextInput)();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.restoreFontSize)();\r\n } else {\r\n const container = document.getElementById('messages-panel');\r\n const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;\r\n const shouldScrollToBottom = distanceFromBottom <= 300;\r\n if (chat.maximizeResizeHandler) {\r\n window.removeEventListener('resize', chat.maximizeResizeHandler);\r\n delete chat.maximizeResizeHandler;\r\n }\r\n if (originalChatState) {\r\n chat.style.width = `${originalChatState.width}px`;\r\n chat.style.height = `${originalChatState.height}px`;\r\n chat.style.left = `${originalChatState.left}px`;\r\n chat.style.maxWidth = '';\r\n chat.style.minWidth = '';\r\n chat.style.position = 'fixed';\r\n chat.style.right = '';\r\n chat.style.margin = '';\r\n chat.style.transform = '';\r\n chat.style.top = 'auto';\r\n if (originalChatState.floating) {\r\n const viewportHeight = window.innerHeight;\r\n const proposedTop = originalChatState.top;\r\n if (proposedTop + originalChatState.height <= viewportHeight) {\r\n chat.style.top = `${proposedTop}px`;\r\n } else {\r\n chat.style.bottom = '0';\r\n chat.style.top = 'auto';\r\n }\r\n } else {\r\n chat.style.bottom = '0';\r\n chat.style.top = '';\r\n }\r\n const currentState = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.getChatState)();\r\n const newState = {\r\n ...currentState,\r\n width: originalChatState.width,\r\n height: originalChatState.height,\r\n left: originalChatState.left,\r\n top: originalChatState.top,\r\n floating: originalChatState.floating,\r\n isVisible: originalChatState.isVisible,\r\n };\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.saveChatState)(newState);\r\n }\r\n chat.classList.remove('maximized');\r\n maximizeButton.classList.remove('maximized');\r\n maximizeButton.innerHTML = _icons_js__WEBPACK_IMPORTED_MODULE_2__.expandSVG;\r\n requestAnimationFrame(() => {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.handleElementsBehavior)();\r\n if (shouldScrollToBottom) {\r\n container.scrollTop = container.scrollHeight;\r\n }\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.focusTextInput)();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.restoreFontSize)();\r\n });\r\n }\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/chatUI.js?");
/***/ }),
/***/ "./src/components/emojiPanel.js":
/*!**************************************!*\
!*** ./src/components/emojiPanel.js ***!
\**************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ EmojiPanel: () => (/* binding */ EmojiPanel)\n/* harmony export */ });\n/* harmony import */ var _data_emojiData_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../data/emojiData.js */ \"./src/data/emojiData.js\");\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../helpers.js */ \"./src/helpers.js\");\n\r\n\r\n\r\n// Create a single global shortcut handler\r\nconst setupGlobalEmojiShortcut = (() => {\r\n let isSetup = false;\r\n\r\n return function () {\r\n if (!isSetup) {\r\n document.addEventListener('keydown', (e) => {\r\n if (e.ctrlKey && e.code === 'Semicolon') {\r\n e.preventDefault();\r\n if (!document.querySelector('.emoji-panel')) {\r\n // Initialize or show the emoji panel with a callback to insert emoji\r\n new EmojiPanel({\r\n onEmojiSelect: (emoji) => {\r\n const input = document.getElementById('message-input');\r\n if (input) {\r\n input.value += emoji;\r\n // Optionally, dispatch an input event if your application needs it\r\n input.dispatchEvent(new Event('input', { bubbles: true }));\r\n input.focus(); // Add this line to restore focus\r\n }\r\n }\r\n }).show();\r\n }\r\n }\r\n });\r\n isSetup = true;\r\n }\r\n };\r\n})();\r\n\r\n// Call this function immediately to set up the shortcut\r\nsetupGlobalEmojiShortcut();\r\n\r\n\r\nclass EmojiPanel {\r\n static instance = null;\r\n\r\n constructor(options = {}) {\r\n if (EmojiPanel.instance) {\r\n return EmojiPanel.instance;\r\n }\r\n EmojiPanel.instance = this;\r\n this.options = {\r\n onEmojiSelect: options.onEmojiSelect || (() => { }),\r\n container: options.container || document.getElementById('messages-panel') || document.body,\r\n position: options.position || 'bottom',\r\n onDestroy: options.onDestroy,\r\n emojiButton: options.emojiButton,\r\n };\r\n\r\n // DOM elements\r\n this.container = null;\r\n this.searchInput = null;\r\n this.emojiContainer = null;\r\n this.categoryNav = null;\r\n this.infoPanel = null;\r\n this.infoIcon = null;\r\n this.infoKeywords = null;\r\n this.languageSelect = null;\r\n\r\n // Category definitions with icons\r\n this.categories = {\r\n recent: { icon: '🕒' },\r\n smileys: { icon: '😊' },\r\n nature: { icon: '🦊' },\r\n food: { icon: '🍔' },\r\n activities: { icon: '⚽' },\r\n travel: { icon: '✈️' },\r\n objects: { icon: '💡' },\r\n symbols: { icon: '💕' },\r\n flags: { icon: '🎌' }\r\n };\r\n\r\n // Localized category names\r\n this.categoryLabels = {\r\n en: {\r\n recent: 'Recently Used',\r\n smileys: 'Smileys & People',\r\n nature: 'Animals & Nature',\r\n food: 'Food & Drink',\r\n activities: 'Activities',\r\n travel: 'Travel & Places',\r\n objects: 'Objects',\r\n symbols: 'Symbols',\r\n flags: 'Flags'\r\n },\r\n ru: {\r\n recent: 'Недавно использованные',\r\n smileys: 'Смайлы и люди',\r\n nature: 'Животные и природа',\r\n food: 'Еда и напитки',\r\n activities: 'Активности',\r\n travel: 'Путешествия и места',\r\n objects: 'Объекты',\r\n symbols: 'Символы',\r\n flags: 'Флаги'\r\n }\r\n };\r\n\r\n // UI Labels for static text elements\r\n this.uiLabels = {\r\n en: {\r\n searchResults: 'Search Results'\r\n },\r\n ru: {\r\n searchResults: 'Результаты поиска'\r\n }\r\n };\r\n\r\n // Emoji data and keywords\r\n this.emojiData = _data_emojiData_js__WEBPACK_IMPORTED_MODULE_0__.emojiData;\r\n this.emojiKeywords = _data_emojiData_js__WEBPACK_IMPORTED_MODULE_0__.emojiKeywords;\r\n\r\n // Recently used emojis (loaded from localStorage)\r\n this.recentEmojis = this.loadRecentEmojis();\r\n\r\n // Infinite scroll settings\r\n this.batchSize = 50;\r\n this.loadedIndices = {};\r\n this.categorySections = {};\r\n\r\n // Retrieve current language from localStorage (default to 'en')\r\n this.currentLanguage = localStorage.getItem('emojiPanelLanguage') || 'en';\r\n }\r\n\r\n /** Initialize the emoji panel */\r\n init() {\r\n this.createPanel();\r\n this.bindEvents();\r\n this.loadAllEmojis();\r\n return this;\r\n }\r\n\r\n /** Create the HTML structure of the emoji panel */\r\n createPanel() {\r\n if (document.querySelector('.emoji-panel')) {\r\n return;\r\n }\r\n this.container = document.createElement('div');\r\n this.container.className = 'emoji-panel';\r\n\r\n // Search container\r\n const searchContainer = document.createElement('div');\r\n searchContainer.className = 'emoji-search-container';\r\n this.searchInput = document.createElement('input');\r\n this.searchInput.type = 'search';\r\n this.searchInput.className = 'emoji-search';\r\n searchContainer.appendChild(this.searchInput);\r\n\r\n // Category navigation\r\n this.categoryNav = document.createElement('div');\r\n this.categoryNav.className = 'emoji-categories';\r\n Object.entries(this.categories).forEach(([key, category]) => {\r\n const categoryBtn = document.createElement('button');\r\n categoryBtn.className = 'emoji-category-btn';\r\n categoryBtn.dataset.category = key;\r\n categoryBtn.innerHTML = category.icon;\r\n categoryBtn.title = this.getLocalizedCategoryName(key);\r\n this.categoryNav.appendChild(categoryBtn);\r\n });\r\n\r\n // Emoji grid container\r\n this.emojiContainer = document.createElement('div');\r\n this.emojiContainer.className = 'emoji-container';\r\n\r\n // Info panel\r\n this.infoPanel = document.createElement('div');\r\n this.infoPanel.className = 'emoji-info-panel';\r\n this.infoIcon = document.createElement('span');\r\n this.infoIcon.className = 'emoji-info-icon';\r\n this.infoKeywords = document.createElement('span');\r\n this.infoKeywords.className = 'emoji-info-keywords';\r\n this.infoPanel.appendChild(this.infoIcon);\r\n this.infoPanel.appendChild(this.infoKeywords);\r\n\r\n // Language selector\r\n this.languageSelect = document.createElement('select');\r\n this.languageSelect.className = 'emoji-language-select';\r\n this.languageSelect.innerHTML = `\r\n <option value=\"en\">EN</option>\r\n <option value=\"ru\">RU</option>\r\n `;\r\n this.languageSelect.value = this.currentLanguage;\r\n\r\n // Footer to hold info panel and language selector\r\n const footer = document.createElement('div');\r\n footer.className = 'emoji-footer';\r\n footer.appendChild(this.infoPanel);\r\n footer.appendChild(this.languageSelect);\r\n\r\n // Assemble the panel\r\n this.container.appendChild(searchContainer);\r\n this.container.appendChild(this.categoryNav);\r\n this.container.appendChild(this.emojiContainer);\r\n this.container.appendChild(footer);\r\n this.options.container.appendChild(this.container);\r\n // Fade in the panel\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.adjustVisibility)(this.container, 'show', '1');\r\n this.searchInput.focus();\r\n }\r\n\r\n /** Load initial batch of emojis for all categories */\r\n loadAllEmojis() {\r\n if (!this.emojiContainer) return; // Exit early if container is missing\r\n this.emojiContainer.innerHTML = '';\r\n this.loadedIndices = {};\r\n this.categorySections = {};\r\n Object.keys(this.categories).forEach(category => {\r\n const section = document.createElement('div');\r\n section.className = 'emoji-category-section';\r\n section.id = `emoji-section-${category}`;\r\n const header = document.createElement('div');\r\n header.className = 'emoji-category-header';\r\n header.textContent = this.getLocalizedCategoryName(category);\r\n section.appendChild(header);\r\n const emojiList = document.createElement('div');\r\n emojiList.className = 'emoji-list';\r\n section.appendChild(emojiList);\r\n this.emojiContainer.appendChild(section);\r\n this.loadedIndices[category] = 0;\r\n this.categorySections[category] = { section, emojiList, header };\r\n this.loadMoreEmojisForCategory(category);\r\n });\r\n }\r\n\r\n /** Load more emojis for a specific category */\r\n loadMoreEmojisForCategory(category) {\r\n const data = category === 'recent' ? this.recentEmojis : (this.emojiData[category] || []);\r\n const start = this.loadedIndices[category];\r\n const batch = data.slice(start, start + this.batchSize);\r\n if (!batch.length) return;\r\n const container = this.categorySections[category].emojiList;\r\n batch.forEach(emoji => {\r\n const button = document.createElement('button');\r\n button.className = 'emoji-btn';\r\n button.textContent = emoji;\r\n button.addEventListener('mouseenter', () => this.updateInfoPanel(emoji));\r\n button.addEventListener('mouseleave', () => this.clearInfoPanel());\r\n button.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (e.shiftKey && category === 'recent') {\r\n e.preventDefault();\r\n this.removeFromRecent(emoji);\r\n } else {\r\n this.addToRecent(emoji);\r\n this.options.onEmojiSelect(emoji);\r\n if (!e.ctrlKey) {\r\n this.destroy();\r\n } else {\r\n this.searchInput.focus();\r\n }\r\n }\r\n });\r\n container.appendChild(button);\r\n });\r\n this.loadedIndices[category] += batch.length;\r\n }\r\n\r\n /** Search for emojis based on a search term */\r\n searchEmojis(searchTerm) {\r\n if (!this.emojiContainer) return; // Exit early if container is missing\r\n this.emojiContainer.innerHTML = '';\r\n const section = document.createElement('div');\r\n section.className = 'emoji-category-section';\r\n const header = document.createElement('div');\r\n header.className = 'emoji-category-header';\r\n header.textContent = this.uiLabels[this.currentLanguage].searchResults;\r\n section.appendChild(header);\r\n const emojiList = document.createElement('div');\r\n emojiList.className = 'emoji-list';\r\n section.appendChild(emojiList);\r\n const results = [];\r\n Object.keys(this.emojiData).forEach(category => {\r\n const emojis = this.emojiData[category] || [];\r\n emojis.forEach(emoji => {\r\n const keywordsObj = this.emojiKeywords[emoji] || {};\r\n const allKeywords = Object.values(keywordsObj).flat();\r\n if (\r\n emoji.includes(searchTerm) ||\r\n allKeywords.some(keyword => keyword.toLowerCase().includes(searchTerm))\r\n ) {\r\n results.push(emoji);\r\n }\r\n });\r\n });\r\n results.forEach(emoji => {\r\n const button = document.createElement('button');\r\n button.className = 'emoji-btn';\r\n button.textContent = emoji;\r\n button.addEventListener('mouseenter', () => this.updateInfoPanel(emoji));\r\n button.addEventListener('mouseleave', () => this.clearInfoPanel());\r\n button.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.addToRecent(emoji);\r\n this.options.onEmojiSelect(emoji);\r\n if (!e.ctrlKey) {\r\n this.destroy();\r\n } else {\r\n this.searchInput.focus();\r\n }\r\n });\r\n emojiList.appendChild(button);\r\n });\r\n this.emojiContainer.appendChild(section);\r\n }\r\n\r\n /** Bind event listeners for the emoji panel */\r\n bindEvents() {\r\n // Close panel on clicking outside\r\n this._documentClickHandler = (e) => {\r\n if (!this.container.contains(e.target)) {\r\n this.destroy();\r\n }\r\n };\r\n document.addEventListener('click', this._documentClickHandler);\r\n\r\n // Close panel on Escape key\r\n this._emojiKeydownHandler = (e) => {\r\n if (e.key === 'Escape') {\r\n this.destroy();\r\n }\r\n };\r\n document.addEventListener('keydown', this._emojiKeydownHandler);\r\n\r\n // Handle 'q' key for closing panel\r\n this._qKeydownHandler = (e) => {\r\n if (e.code === 'KeyQ') {\r\n if (document.activeElement === this.searchInput) {\r\n const now = Date.now();\r\n if (this._lastQPressTime && (now - this._lastQPressTime < 500)) {\r\n e.preventDefault();\r\n this.destroy();\r\n this._lastQPressTime = 0;\r\n } else {\r\n this._lastQPressTime = now;\r\n }\r\n } else {\r\n e.preventDefault();\r\n this.destroy();\r\n }\r\n }\r\n };\r\n document.addEventListener('keydown', this._qKeydownHandler);\r\n\r\n // Update view on search input change\r\n this.searchInput.addEventListener('input', (e) => {\r\n const searchTerm = e.target.value.trim().toLowerCase();\r\n if (searchTerm) {\r\n this.searchEmojis(searchTerm);\r\n this.emojiContainer.classList.add('search-active');\r\n } else {\r\n this.loadAllEmojis();\r\n this.emojiContainer.classList.remove('search-active');\r\n }\r\n });\r\n\r\n // Handle Enter key in search input\r\n this.searchInput.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter') {\r\n e.preventDefault();\r\n\r\n // Ensure both elements exist before proceeding\r\n if (!this.searchInput || !this.emojiContainer) return;\r\n\r\n if (this.emojiContainer.classList.contains('search-active')) {\r\n const firstEmojiBtn = this.emojiContainer?.querySelector('.emoji-btn');\r\n\r\n if (firstEmojiBtn) {\r\n const clickEvent = new MouseEvent('click', {\r\n bubbles: true,\r\n cancelable: true,\r\n ctrlKey: e.ctrlKey\r\n });\r\n firstEmojiBtn.dispatchEvent(clickEvent);\r\n\r\n // Ensure emojiContainer still exists before modifying it\r\n if (this.emojiContainer) {\r\n this.emojiContainer.classList.remove('search-active');\r\n this.loadAllEmojis(); // Only if emojiContainer is still valid\r\n }\r\n\r\n // Ensure the search input exists before modifying it\r\n if (this.searchInput) {\r\n this.searchInput.value = '';\r\n }\r\n\r\n if (!e.ctrlKey) {\r\n this.destroy();\r\n } else if (this.searchInput) {\r\n this.searchInput.focus();\r\n }\r\n }\r\n }\r\n }\r\n });\r\n\r\n // Category navigation clicks\r\n this.categoryNav.addEventListener('click', (e) => {\r\n const btn = e.target.closest('.emoji-category-btn');\r\n if (btn) {\r\n const category = btn.dataset.category;\r\n const section = document.getElementById(`emoji-section-${category}`);\r\n if (section && this.emojiContainer) {\r\n // Add smooth scrolling behavior\r\n this.emojiContainer.style.scrollBehavior = 'smooth';\r\n\r\n // Instead of using scrollIntoView, manually set the scrollTop\r\n // of the emoji container to the offsetTop of the section\r\n this.emojiContainer.scrollTop = section.offsetTop - this.emojiContainer.offsetTop;\r\n\r\n // Prevent default behavior to avoid any parent scrolling\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }\r\n }\r\n });\r\n\r\n // Infinite scroll handler\r\n this.emojiContainer.addEventListener('scroll', () => this.handleScroll());\r\n\r\n // Language change handler\r\n this.languageSelect.addEventListener('change', (e) => {\r\n this.currentLanguage = e.target.value;\r\n localStorage.setItem('emojiPanelLanguage', this.currentLanguage);\r\n const currentEmoji = this.infoIcon.textContent;\r\n if (currentEmoji) {\r\n this.updateInfoPanel(currentEmoji);\r\n }\r\n this.updateCategoryLabels();\r\n if (this.searchInput.value.trim()) {\r\n this.searchEmojis(this.searchInput.value.trim().toLowerCase());\r\n }\r\n });\r\n\r\n // Prevent closing when clicking inside panel\r\n this.container.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n });\r\n }\r\n\r\n /** Handle scroll events for infinite scrolling and category highlighting */\r\n handleScroll() {\r\n Object.keys(this.categorySections).forEach(category => {\r\n const { section } = this.categorySections[category];\r\n const sectionRect = section.getBoundingClientRect();\r\n const containerRect = this.emojiContainer.getBoundingClientRect();\r\n if (sectionRect.bottom < containerRect.bottom + 100) {\r\n this.loadMoreEmojisForCategory(category);\r\n }\r\n });\r\n let activeCategory = null;\r\n let minDistance = Infinity;\r\n Object.keys(this.categorySections).forEach(category => {\r\n const headerRect = this.categorySections[category].header.getBoundingClientRect();\r\n const containerRect = this.emojiContainer.getBoundingClientRect();\r\n const distance = Math.abs(headerRect.top - containerRect.top);\r\n if (headerRect.top <= containerRect.top + 10 && distance < minDistance) {\r\n minDistance = distance;\r\n activeCategory = category;\r\n }\r\n });\r\n if (activeCategory) {\r\n this.highlightCategory(activeCategory);\r\n }\r\n }\r\n\r\n /** Update category labels based on current language */\r\n updateCategoryLabels() {\r\n Object.keys(this.categories).forEach(category => {\r\n const localizedName = this.getLocalizedCategoryName(category);\r\n const btn = this.categoryNav.querySelector(`[data-category=\"${category}\"]`);\r\n if (btn) btn.title = localizedName;\r\n if (this.categorySections[category] && this.categorySections[category].header) {\r\n this.categorySections[category].header.textContent = localizedName;\r\n }\r\n });\r\n }\r\n\r\n /** Update the info panel with emoji and keywords */\r\n updateInfoPanel(emoji) {\r\n // Check if the info panel elements exist before trying to update them\r\n if (!this.infoIcon || !this.infoKeywords) return;\r\n\r\n this.infoIcon.textContent = emoji;\r\n const keywordsObj = this.emojiKeywords[emoji] || {};\r\n const keywords = keywordsObj[this.currentLanguage] || [];\r\n this.infoKeywords.textContent = keywords.join(', ');\r\n }\r\n\r\n /** Clear the info panel */\r\n clearInfoPanel() {\r\n // Check if the info panel elements exist before trying to clear them\r\n if (!this.infoIcon || !this.infoKeywords) return;\r\n\r\n this.infoIcon.textContent = '';\r\n this.infoKeywords.textContent = '';\r\n }\r\n\r\n /** Add an emoji to the recent list and refresh the recent section */\r\n addToRecent(emoji) {\r\n this.recentEmojis = [\r\n emoji,\r\n ...this.recentEmojis.filter(e => e !== emoji)\r\n ].slice(0, 25);\r\n this.saveRecentEmojis();\r\n if (this.categorySections['recent']) {\r\n const recentList = this.categorySections['recent'].emojiList;\r\n recentList.innerHTML = '';\r\n this.loadedIndices['recent'] = 0;\r\n this.loadMoreEmojisForCategory('recent');\r\n }\r\n }\r\n\r\n /** Remove an emoji from the recent list and refresh the recent section */\r\n removeFromRecent(emoji) {\r\n this.recentEmojis = this.recentEmojis.filter(e => e !== emoji);\r\n this.saveRecentEmojis();\r\n if (this.categorySections['recent']) {\r\n const recentList = this.categorySections['recent'].emojiList;\r\n recentList.innerHTML = '';\r\n this.loadedIndices['recent'] = 0;\r\n this.loadMoreEmojisForCategory('recent');\r\n }\r\n }\r\n\r\n /** Load recent emojis from localStorage */\r\n loadRecentEmojis() {\r\n try {\r\n return JSON.parse(localStorage.getItem('recentEmojis')) || [];\r\n } catch {\r\n return [];\r\n }\r\n }\r\n\r\n /** Save recent emojis to localStorage */\r\n saveRecentEmojis() {\r\n try {\r\n localStorage.setItem('recentEmojis', JSON.stringify(this.recentEmojis));\r\n } catch (e) {\r\n console.error('Failed to save recent emojis:', e);\r\n }\r\n }\r\n\r\n /** Highlight the active category in the navigation */\r\n highlightCategory(categoryId) {\r\n const buttons = this.categoryNav.querySelectorAll('.emoji-category-btn');\r\n buttons.forEach(btn => {\r\n btn.classList.toggle('active', btn.dataset.category === categoryId);\r\n });\r\n }\r\n\r\n /** Show the emoji panel */\r\n show() {\r\n this.createPanel();\r\n this.bindEvents();\r\n this.loadAllEmojis();\r\n this.searchInput.focus();\r\n }\r\n\r\n /** Completely remove the emoji panel from DOM and clean up */\r\n destroy() {\r\n document.removeEventListener('keydown', this._emojiKeydownHandler);\r\n document.removeEventListener('keydown', this._qKeydownHandler);\r\n document.removeEventListener('click', this._documentClickHandler);\r\n if (this.container) {\r\n // Fade out the panel; the helper will remove it after transition.\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.adjustVisibility)(this.container, 'hide', '0');\r\n }\r\n this.container = null;\r\n this.searchInput = null;\r\n this.emojiContainer = null;\r\n this.categoryNav = null;\r\n this.infoPanel = null;\r\n this.infoIcon = null;\r\n this.infoKeywords = null;\r\n this.languageSelect = null;\r\n EmojiPanel.instance = null;\r\n if (this.options.emojiButton) {\r\n this.options.emojiButton.title = 'Open emoji picker';\r\n }\r\n if (typeof this.options.onDestroy === 'function') {\r\n this.options.onDestroy();\r\n }\r\n }\r\n\r\n /** Toggle the visibility of the emoji panel */\r\n toggle() {\r\n if (document.querySelector('.emoji-panel')) {\r\n this.destroy();\r\n } else {\r\n this.show();\r\n }\r\n }\r\n\r\n /** Get the localized name for a category */\r\n getLocalizedCategoryName(categoryKey) {\r\n return this.categoryLabels[this.currentLanguage][categoryKey] || categoryKey;\r\n }\r\n}\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/components/emojiPanel.js?");
/***/ }),
/***/ "./src/components/helpPanel.js":
/*!*************************************!*\
!*** ./src/components/helpPanel.js ***!
\*************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ HelpPanel: () => (/* binding */ HelpPanel)\n/* harmony export */ });\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../helpers.js */ \"./src/helpers.js\");\n\r\n\r\nclass HelpPanel {\r\n constructor(options = {}) {\r\n this.container = null;\r\n this.options = {\r\n container: options.container || document.body,\r\n helpButton: options.helpButton,\r\n onDestroy: options.onDestroy\r\n };\r\n // Set the class instance to this\r\n HelpPanel.instance = this;\r\n }\r\n\r\n init() {\r\n this.createPanel();\r\n this.bindEvents();\r\n return this;\r\n }\r\n\r\n createPanel() {\r\n this.container = document.createElement('div');\r\n this.container.className = 'help-panel';\r\n this.content = document.createElement('div');\r\n this.content.className = 'help-content';\r\n this.updatePanelContent();\r\n this.container.appendChild(this.content);\r\n document.body.appendChild(this.container);\r\n // Fade in the panel\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(this.container, 'show', '1');\r\n }\r\n\r\n updatePanelContent() {\r\n const lang = localStorage.getItem('emojiPanelLanguage') || 'en';\r\n const helpTranslations = {\r\n en: {\r\n heading: \"Chat Commands & Hotkeys\",\r\n sections: [\r\n {\r\n title: \"Chat Commands\",\r\n items: [\r\n { key: \"/help\", desc: \"Show this help panel\" },\r\n { key: \"/me message\", desc: \"Send an action message\" },\r\n { key: \"/pm username\", desc: \"Activate private chat mode with the specified user\" },\r\n { key: \"/exit\", desc: \"Exit private chat mode\" }\r\n ]\r\n },\r\n {\r\n title: \"Chat Hotkeys\",\r\n items: [\r\n { key: \"Ctrl + Space\", desc: \"Hide/Show the chat\" },\r\n { key: \"Shift + Ctrl + Space\", desc: \"Expand/Collapse the chat\" },\r\n { key: \"Ctrl + Click\", desc: \"Activate private chat mode with the clicked user\" }\r\n ]\r\n },\r\n {\r\n heading: \"Emoji Panel Actions & Hotkeys\",\r\n subSections: [\r\n {\r\n title: \"Emoji Panel Actions\",\r\n items: [\r\n { key: \"Click an emoji\", desc: \"Insert the emoji\" },\r\n { key: \"Click outside panel\", desc: \"Closes the panel (emoji or help)\" }\r\n ]\r\n },\r\n {\r\n title: \"Emoji Panel Hotkeys\",\r\n items: [\r\n { key: \"Ctrl + ;\", desc: \"Open the Emoji Panel\" },\r\n { key: \"Enter\", desc: \"Insert the emoji\" },\r\n { key: \"Ctrl + Enter\", desc: \"Insert the emoji keeping the panel open\" },\r\n { key: \"Ctrl + Click\", desc: \"Insert the emoji keeping the panel open\" },\r\n { key: \"Shift + Click\", desc: \"Remove emoji from recent list (in recent category)\" },\r\n { key: \"q\", desc: \"Hide the Emoji Panel (single press when search is not focused)\" },\r\n { key: \"qq\", desc: \"Hide the Emoji Panel (double press 'q' when search is focused)\" },\r\n { key: \"Esc\", desc: \"Close the panel (emoji or help)\" }\r\n ]\r\n }\r\n ]\r\n },\r\n {\r\n heading: \"Image Manipulations\",\r\n subSections: [\r\n {\r\n title: \"Open/Close\",\r\n items: [\r\n { key: \"(LMB) Click\", desc: \"Open the image\" },\r\n { key: \"Ctrl + (RMB)\", desc: \"Close the image and copy the link\" },\r\n { key: \"Space or ESC\", desc: \"Close the image\" }\r\n ]\r\n },\r\n {\r\n title: \"Movement and Scaling\",\r\n items: [\r\n { key: \"Hold (MMB)\", desc: \"Drag the expanded image\" },\r\n { key: \"Scroll (MMB)\", desc: \"Zoom in/out the image\" },\r\n { key: \"Ctrl + (MMB)\", desc: \"Scale the image. Move the cursor up or down.\" }\r\n ]\r\n },\r\n {\r\n title: \"Navigation\",\r\n items: [\r\n { key: \"Arrow keys (< >)\", desc: \"Switch between images\" },\r\n { key: \"(LMB), (RMB)\", desc: \"Switch between images\" }\r\n ]\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n ru: {\r\n heading: \"Команды чата и горячие клавиши\",\r\n sections: [\r\n {\r\n title: \"Команды чата\",\r\n items: [\r\n { key: \"/help\", desc: \"Показать панель помощи\" },\r\n { key: \"/me сообщение\", desc: \"Отправить сообщение действия\" },\r\n { key: \"/pm username\", desc: \"Активировать приватный чат для указанного пользователя\" },\r\n { key: \"/exit\", desc: \"Выйти из приватного чата\" }\r\n ]\r\n },\r\n {\r\n title: \"Горячие клавиши чата\",\r\n items: [\r\n { key: \"Ctrl + Space\", desc: \"Скрыть/Показать чат\" },\r\n { key: \"Shift + Ctrl + Space\", desc: \"Развернуть/Свернуть чат\" },\r\n { key: \"Ctrl + Click\", desc: \"Активировать приватный чат для выбранного пользователя\" }\r\n ]\r\n },\r\n {\r\n heading: \"Действия и горячие клавиши панели эмодзи\",\r\n subSections: [\r\n {\r\n title: \"Действия панели эмодзи\",\r\n items: [\r\n { key: \"Click an emoji\", desc: \"Вставить эмодзи\" },\r\n { key: \"Click outside panel\", desc: \"Закрыть панель (эмодзи или помощь)\" }\r\n ]\r\n },\r\n {\r\n title: \"Горячие клавиши панели эмодзи\",\r\n items: [\r\n { key: \"Ctrl + ;\", desc: \"Открыть панель эмодзи\" },\r\n { key: \"Enter\", desc: \"Вставить эмодзи\" },\r\n { key: \"Ctrl + Enter\", desc: \"Вставить эмодзи, оставив панель открытой\" },\r\n { key: \"Ctrl + Click\", desc: \"Вставить эмодзи, оставив панель открытой\" },\r\n { key: \"Shift + Click\", desc: \"Удалить эмодзи из списка \\\"Недавно использованные\\\"\" },\r\n { key: \"q\", desc: \"Скрыть панель эмодзи (одиночный нажим, когда поиск не в фокусе)\" },\r\n { key: \"qq\", desc: \"Скрыть панель эмодзи (дважды нажмите 'q', когда поиск в фокусе)\" },\r\n { key: \"Esc\", desc: \"Закрыть (эмодзи или помощь)\" }\r\n ]\r\n }\r\n ]\r\n },\r\n {\r\n heading: \"Манипуляции с изображением\",\r\n subSections: [\r\n {\r\n title: \"Открытие/Закрытие\",\r\n items: [\r\n { key: \"(ЛКМ) Клик\", desc: \"Открыть изображение\" },\r\n { key: \"Ctrl + (ПКМ)\", desc: \"Закрыть изображение и скопировать ссылку\" },\r\n { key: \"Space или ESC\", desc: \"Закрыть изображение\" }\r\n ]\r\n },\r\n {\r\n title: \"Перемещение и масштабирование\",\r\n items: [\r\n { key: \"Зажатая (СКМ)\", desc: \"Перемещайте развернутое изображение\" },\r\n { key: \"Прокрутка (СКМ)\", desc: \"Увеличивайте/уменьшайте изображение\" },\r\n { key: \"Ctrl + (СКМ)\", desc: \"Масштабируйте изображение. Курсор вверх или вниз.\" }\r\n ]\r\n },\r\n {\r\n title: \"Навигация\",\r\n items: [\r\n { key: \"Стрелки (< >)\", desc: \"Переключение между изображениями\" },\r\n { key: \"(ЛКМ), (ПКМ)\", desc: \"Переключение между изображениями\" }\r\n ]\r\n }\r\n ]\r\n }\r\n ]\r\n }\r\n };\r\n const t = helpTranslations[lang];\r\n let html = `<h5 class=\"help-section-header\">${t.heading}</h5>`;\r\n t.sections.forEach(section => {\r\n if (section.title) {\r\n html += `<h6 class=\"help-section-subheader\">${section.title}</h6>`;\r\n } else if (section.heading) {\r\n html += `<h5 class=\"help-section-header\">${section.heading}</h5>`;\r\n }\r\n if (section.items) {\r\n html += `<ul class=\"help-list\">`;\r\n section.items.forEach(item => {\r\n html += `<li class=\"help-list-item\"><strong class=\"help-hotkey\">${item.key}</strong> ${item.desc}</li>`;\r\n });\r\n html += `</ul>`;\r\n }\r\n if (section.subSections) {\r\n section.subSections.forEach(sub => {\r\n html += `<h6 class=\"help-section-subheader\">${sub.title}</h6>`;\r\n html += `<ul class=\"help-list\">`;\r\n sub.items.forEach(item => {\r\n html += `<li class=\"help-list-item\"><strong class=\"help-hotkey\">${item.key}</strong> ${item.desc}</li>`;\r\n });\r\n html += `</ul>`;\r\n });\r\n }\r\n });\r\n this.content.innerHTML = html;\r\n }\r\n\r\n bindEvents() {\r\n this._clickOutsideHandler = (e) => {\r\n if (this.options.helpButton &&\r\n (e.target === this.options.helpButton || this.options.helpButton.contains(e.target))) {\r\n return;\r\n }\r\n if (this.container && !this.container.contains(e.target)) {\r\n this.remove();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.showChatAlert)('Help panel has been closed.', {\r\n type: 'warning',\r\n duration: 2000\r\n });\r\n }\r\n };\r\n document.addEventListener('click', this._clickOutsideHandler, true);\r\n\r\n this._escHandler = (e) => {\r\n if (e.key === 'Escape') {\r\n this.remove();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.showChatAlert)('Help panel has been closed.', {\r\n type: 'warning',\r\n duration: 2000\r\n });\r\n }\r\n };\r\n document.addEventListener('keydown', this._escHandler, true);\r\n\r\n this._stopPropagationHandler = (e) => {\r\n e.stopPropagation();\r\n };\r\n this.container.addEventListener('click', this._stopPropagationHandler);\r\n }\r\n\r\n remove() {\r\n if (this._clickOutsideHandler) {\r\n document.removeEventListener('click', this._clickOutsideHandler, true);\r\n this._clickOutsideHandler = null;\r\n }\r\n if (this._escHandler) {\r\n document.removeEventListener('keydown', this._escHandler, true);\r\n this._escHandler = null;\r\n }\r\n if (this.container) {\r\n this.container.removeEventListener('click', this._stopPropagationHandler);\r\n this._stopPropagationHandler = null;\r\n }\r\n if (this.container) {\r\n // Fade out the panel; the helper will remove the element after transition.\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(this.container, 'hide', '0');\r\n this.container = null;\r\n }\r\n if (typeof this.options.onDestroy === 'function') {\r\n this.options.onDestroy();\r\n }\r\n HelpPanel.instance = null;\r\n }\r\n\r\n show() {\r\n if (!this.container) {\r\n this.init();\r\n } else {\r\n this.updatePanelContent();\r\n }\r\n if (!document.body.contains(this.container)) {\r\n document.body.appendChild(this.container);\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(this.container, 'show', '1');\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.showChatAlert)(\"Help panel is now visible.\");\r\n }\r\n }\r\n\r\n toggle() {\r\n if (this.container && document.body.contains(this.container)) {\r\n this.remove();\r\n } else {\r\n this.show();\r\n }\r\n }\r\n\r\n static setupHelpCommandEvents() {\r\n const input = document.getElementById('message-input');\r\n if (input) {\r\n input.addEventListener('keydown', (e) => {\r\n if (input.value.trim() === \"/help\" && e.code === 'Space') {\r\n e.preventDefault();\r\n if (!HelpPanel.instance) {\r\n const helpPanelInstance = new HelpPanel({\r\n onDestroy: () => {\r\n // Callback if needed.\r\n }\r\n });\r\n helpPanelInstance.init();\r\n helpPanelInstance.show();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.showChatAlert)(\"Help panel is now visible.\");\r\n } else {\r\n HelpPanel.instance.remove();\r\n }\r\n input.value = \"\";\r\n }\r\n });\r\n }\r\n return HelpPanel.instance;\r\n }\r\n}\r\n\r\n// Static instance tracker\r\nHelpPanel.instance = null;\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/components/helpPanel.js?");
/***/ }),
/***/ "./src/converters/image-converter.js":
/*!*******************************************!*\
!*** ./src/converters/image-converter.js ***!
\*******************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ convertImageLinksToImage: () => (/* binding */ convertImageLinksToImage)\n/* harmony export */ });\n/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../helpers */ \"./src/helpers.js\");\n/* harmony import */ var _definitions__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../definitions */ \"./src/definitions.js\");\n// helpers\r\n // helpers\r\n\r\n// definitions\r\n\r\n\r\n// Image constants\r\nconst imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\r\nconst emojis = { image: '📸', domain: '🖥️', untrusted: '💀️️' };\r\nconst zoomLimits = { min: 0.2, max: 10, factor: 0.1 };\r\nconst navigationDelay = 50;\r\n\r\n// Image navigation state\r\nlet currentIndex = 0;\r\nlet isChangingImage = false;\r\nlet thumbnailLinks = [];\r\n\r\n// Expanded image reference\r\nlet expandedImage = null;\r\n\r\nconst getExtension = (url) => {\r\n try {\r\n return (url.match(/\\.([^?#.]+)(?:[?#]|$)/i)?.[1]?.toLowerCase() || '');\r\n } catch (error) {\r\n console.error(\"Error extracting extension:\", error.message);\r\n return '';\r\n }\r\n};\r\n\r\nconst isAllowedImageExtension = (url) => {\r\n const extension = getExtension(url);\r\n return { allowed: imageExtensions.includes(extension), extension };\r\n};\r\n\r\nconst createExpandedView = (src, clickedThumbnailIndex) => {\r\n // Create and add expanded image to DOM\r\n const imageElement = document.createElement('img');\r\n imageElement.src = src;\r\n imageElement.classList.add('scaled-thumbnail');\r\n\r\n document.body.appendChild(imageElement);\r\n\r\n currentIndex = clickedThumbnailIndex;\r\n\r\n // Zoom and movement variables\r\n let zoomScale = 1;\r\n let isMMBPressed = false;\r\n let lastMouseX = 0,\r\n lastMouseY = 0;\r\n let translateX = 0,\r\n translateY = 0;\r\n const movementSpeed = 5;\r\n\r\n // Get or create the dimming element\r\n let dimmingElement = document.querySelector('.dimming-element');\r\n if (!dimmingElement) {\r\n dimmingElement = document.createElement('div');\r\n dimmingElement.classList.add('dimming-element');\r\n document.body.appendChild(dimmingElement);\r\n }\r\n\r\n // Define closeExpandedView function\r\n const closeExpandedView = (img) => {\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(img, 'hide', '0');\r\n if (!document.querySelector('.popup-panel') && dimmingElement) {\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(dimmingElement, 'hide', '0');\r\n }\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.removeBigImageEventListeners)();\r\n };\r\n\r\n // Define event listeners for the expanded image\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['click'] = (event) => {\r\n if (!imageElement.contains(event.target)) {\r\n imageElement.remove();\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.removeBigImageEventListeners)();\r\n }\r\n };\r\n\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['keydown'] = (event) => {\r\n if (event.code === 'Escape' || event.code === 'Space') {\r\n event.preventDefault();\r\n closeExpandedView(imageElement);\r\n } else if (event.code === 'ArrowLeft') {\r\n navigateImages(-1);\r\n } else if (event.code === 'ArrowRight') {\r\n navigateImages(1);\r\n }\r\n };\r\n\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['wheel'] = (event) => {\r\n const direction = event.deltaY < 0 ? 1 : -1;\r\n zoomScale += direction * zoomLimits.factor * zoomScale;\r\n zoomScale = Math.max(zoomLimits.min, Math.min(zoomScale, zoomLimits.max));\r\n \r\n // Update transform to maintain center positioning while zooming\r\n imageElement.style.transform = `translate(-50%, -50%) translate(${translateX}px, ${translateY}px) scale(${zoomScale})`;\r\n };\r\n\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['mousemove'] = (event) => {\r\n if (isMMBPressed) {\r\n if (event.ctrlKey) {\r\n const deltaY = event.clientY - lastMouseY;\r\n const zoomDirection = deltaY < 0 ? 1 : -1;\r\n const zoomAmount = Math.abs(deltaY) * zoomLimits.factor * 0.05;\r\n zoomScale += zoomDirection * zoomAmount * zoomScale;\r\n zoomScale = Math.max(zoomLimits.min, Math.min(zoomScale, zoomLimits.max));\r\n } else {\r\n const deltaX = (event.clientX - lastMouseX) / zoomScale * movementSpeed;\r\n const deltaY = (event.clientY - lastMouseY) / zoomScale * movementSpeed;\r\n translateX += deltaX;\r\n translateY += deltaY;\r\n }\r\n \r\n // Update transform to maintain center positioning while moving and zooming\r\n imageElement.style.transform = `translate(-50%, -50%) translate(${translateX}px, ${translateY}px) scale(${zoomScale})`;\r\n \r\n lastMouseX = event.clientX;\r\n lastMouseY = event.clientY;\r\n }\r\n };\r\n\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['mousedown'] = (event) => {\r\n const { button, clientX, clientY, target, ctrlKey } = event;\r\n if ((button === 0 || button === 2) && target !== imageElement) return;\r\n if (button === 0) {\r\n navigateImages(-1);\r\n } else if (button === 2) {\r\n event.preventDefault();\r\n if (ctrlKey) {\r\n navigator.clipboard.writeText(target.src).catch(console.error);\r\n closeExpandedView(imageElement);\r\n } else {\r\n navigateImages(1);\r\n }\r\n } else if (button === 1) { // Middle mouse button\r\n isMMBPressed = true;\r\n lastMouseX = clientX;\r\n lastMouseY = clientY;\r\n event.preventDefault();\r\n }\r\n };\r\n\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['mouseup'] = (event) => {\r\n if (event.button === 1) {\r\n isMMBPressed = false;\r\n }\r\n };\r\n\r\n _definitions__WEBPACK_IMPORTED_MODULE_1__.state.bigImageEvents['contextmenu'] = (event) => event.preventDefault();\r\n\r\n // Add the event listeners using your helper function\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.addBigImageEventListeners)();\r\n\r\n // Show the dimming element using adjustVisibility\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(dimmingElement, \"show\", \"1\");\r\n\r\n // When clicking the dimming background, hide the image and the dimming element\r\n dimmingElement.addEventListener('click', () => {\r\n closeExpandedView(imageElement);\r\n });\r\n\r\n return imageElement;\r\n};\r\n\r\nconst navigateImages = (direction) => {\r\n const newIndex = currentIndex + direction;\r\n if (newIndex >= 0 && newIndex < thumbnailLinks.length && !isChangingImage) {\r\n isChangingImage = true;\r\n if (expandedImage) expandedImage.src = thumbnailLinks[newIndex].imgSrc;\r\n setTimeout(() => (isChangingImage = false), navigationDelay);\r\n currentIndex = newIndex;\r\n }\r\n};\r\n\r\nfunction convertImageLinksToImage(containerType) {\r\n const container = document.getElementById('messages-panel');\r\n if (!container) return;\r\n\r\n const refreshThumbnailLinks = () => {\r\n thumbnailLinks = [];\r\n container.querySelectorAll(\".clickable-thumbnail\").forEach((thumbnail, index) => {\r\n const img = thumbnail.querySelector(\"img\");\r\n if (img && thumbnail.dataset.sourceLink) {\r\n thumbnailLinks.push({ link: thumbnail.dataset.sourceLink, imgSrc: img.src, index });\r\n }\r\n });\r\n };\r\n\r\n const links = container.querySelectorAll(\"a:not(.skipped):not(.processed-image)\");\r\n if (!links.length) return;\r\n\r\n links.forEach((link) => {\r\n if (!link.href || !link.href.startsWith(\"http\")) return;\r\n const { allowed, extension } = isAllowedImageExtension(link.href);\r\n if (!allowed) return;\r\n\r\n link.classList.add(\"media\");\r\n const { isTrusted, domain } = (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.isTrustedDomain)(link.href);\r\n link.title = (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.isEncodedURL)(link.href) ? (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.decodeURL)(link.href) : link.href;\r\n\r\n isTrusted\r\n ? handleTrustedLink(link, extension, domain)\r\n : handleUntrustedLink(link, extension, domain);\r\n });\r\n\r\n function createThumbnail(link, isUntrusted) {\r\n const thumbnail = document.createElement(\"div\");\r\n thumbnail.classList.add(\"clickable-thumbnail\");\r\n thumbnail.dataset.sourceLink = link.href;\r\n\r\n const img = document.createElement(\"img\");\r\n img.src = link.href;\r\n\r\n img.onload = () => {\r\n thumbnail.appendChild(img);\r\n link.parentNode.insertBefore(thumbnail, link.nextSibling);\r\n };\r\n\r\n img.onerror = () => {\r\n console.error(\"Failed to load image:\", link.href);\r\n link.classList.add(\"skipped\");\r\n };\r\n\r\n if (isUntrusted) {\r\n if (!link.querySelector(\".clickable-thumbnail\")) {\r\n link.addEventListener(\"click\", (e) => {\r\n if (!link.querySelector(\".clickable-thumbnail\")) {\r\n thumbnail.appendChild(img);\r\n link.parentNode.insertBefore(thumbnail, link.nextSibling);\r\n }\r\n });\r\n }\r\n } else {\r\n thumbnail.appendChild(img);\r\n link.parentNode.insertBefore(thumbnail, link.nextSibling);\r\n }\r\n\r\n thumbnail.addEventListener(\"click\", (e) => {\r\n e.stopPropagation();\r\n refreshThumbnailLinks();\r\n const clickedIndex = thumbnailLinks.findIndex(\r\n (item) => item.link === link.href || item.imgSrc === img.src\r\n );\r\n expandedImage = createExpandedView(\r\n img.src,\r\n clickedIndex >= 0 ? clickedIndex : 0\r\n );\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(expandedImage, \"show\", \"1\");\r\n const dimmingElement = document.querySelector('.dimming-element');\r\n if (dimmingElement) {\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.adjustVisibility)(dimmingElement, \"show\", \"1\");\r\n }\r\n });\r\n }\r\n\r\n function handleUntrustedLink(link, extension, domain) {\r\n link.classList.add(\"skipped\");\r\n link.textContent = `${emojis.image} Image (${extension.toUpperCase()}) ${emojis.domain} Hostname (${domain}) ${emojis.untrusted} Untrusted`;\r\n link.addEventListener(\"click\", (e) => {\r\n if (!link.classList.contains(\"processed-image\")) {\r\n e.preventDefault();\r\n link.classList.remove(\"skipped\");\r\n link.classList.add(\"processed-image\");\r\n createThumbnail(link, true);\r\n }\r\n });\r\n }\r\n\r\n function handleTrustedLink(link, extension, domain) {\r\n link.textContent = `${emojis.image} Image (${extension.toUpperCase()}) ${emojis.domain} Hostname (${domain})`;\r\n link.classList.add(\"processed-image\");\r\n createThumbnail(link, false);\r\n }\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/converters/image-converter.js?");
/***/ }),
/***/ "./src/converters/video-converter.js":
/*!*******************************************!*\
!*** ./src/converters/video-converter.js ***!
\*******************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ convertVideoLinksToPlayer: () => (/* binding */ convertVideoLinksToPlayer)\n/* harmony export */ });\n/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../helpers */ \"./src/helpers.js\");\n // helpers\r\n\r\nconst emojis = { image: '🎥', domain: '🖥️', untrusted: '💀️️' };\r\nconst allowedVideoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi'];\r\n\r\nconst isAllowedVideoExtension = url => {\r\n const ext = url.match(/\\.([^?#.]+)(?:[?#]|$)/i)?.[1]?.toLowerCase() || '';\r\n return { allowed: allowedVideoExtensions.includes(ext), extension: ext };\r\n};\r\n\r\nfunction convertVideoLinksToPlayer(containerType) {\r\n const container = document.getElementById('messages-panel');\r\n if (!container) return;\r\n\r\n const links = container.querySelectorAll(\"a:not(.skipped):not(.processed-video)\");\r\n if (!links.length) return;\r\n\r\n links.forEach(link => {\r\n const url = link.href;\r\n if (!url) return;\r\n\r\n const videoInfo = getVideoInfo(url);\r\n if (!videoInfo) return;\r\n\r\n link.classList.add(\"media\");\r\n const { isTrusted, domain } = (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.isTrustedDomain)(url);\r\n\r\n if (!isTrusted) {\r\n link.classList.add(\"skipped\");\r\n link.textContent = `${emojis.image} ${videoInfo.videoType} ${emojis.domain} Hostname (${domain}) ${emojis.untrusted} Untrusted`;\r\n link.addEventListener(\"click\", e => {\r\n if (!link.classList.contains(\"processed-video\")) {\r\n e.preventDefault();\r\n link.classList.remove(\"skipped\");\r\n processVideoLink(link, url, domain, videoInfo);\r\n }\r\n });\r\n return;\r\n }\r\n\r\n processVideoLink(link, url, domain, videoInfo);\r\n });\r\n\r\n function processVideoLink(link, url, domain, videoInfo) {\r\n const { youtubeMatch, videoType, videoId } = videoInfo;\r\n const videoCheck = isAllowedVideoExtension(url);\r\n if (!youtubeMatch && !videoCheck.allowed) return;\r\n\r\n link.classList.add(\"processed-video\");\r\n const wrapper = document.createElement('div');\r\n wrapper.classList.add(\"video-wrapper\");\r\n\r\n const embed = document.createElement(youtubeMatch ? 'iframe' : 'video');\r\n embed.classList.add(\"video-container\");\r\n\r\n link.textContent = `${emojis.image} ${videoType} ${emojis.domain} Hostname (${domain})`;\r\n\r\n if (youtubeMatch) {\r\n embed.src = `https://www.youtube.com/embed/${videoId}`;\r\n embed.allowFullscreen = true;\r\n } else {\r\n embed.src = url;\r\n embed.controls = true;\r\n }\r\n\r\n link.title = (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.isEncodedURL)(url) ? (0,_helpers__WEBPACK_IMPORTED_MODULE_0__.decodeURL)(url) : url;\r\n link.style.display = 'inline-flex';\r\n\r\n link.parentNode.insertBefore(wrapper, link);\r\n wrapper.append(link, embed);\r\n }\r\n\r\n function getVideoInfo(url) {\r\n const youtubeMatch = url.match(/(?:shorts\\/|live\\/|watch\\?v=|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/i);\r\n\r\n if (youtubeMatch) {\r\n const videoId = youtubeMatch[1];\r\n const videoType = url.includes('shorts/') ? 'Shorts' :\r\n url.includes('live/') ? 'Live' :\r\n url.includes('watch?v=') ? 'Watch' :\r\n url.includes('youtu.be/') ? 'Share' : 'YouTube';\r\n return { youtubeMatch: true, videoId, videoType };\r\n }\r\n\r\n const extension = url.split('.').pop().toLowerCase();\r\n if (allowedVideoExtensions.includes(extension)) {\r\n return { youtubeMatch: false, videoType: `Video (${extension.toUpperCase()})` };\r\n }\r\n\r\n return false;\r\n }\r\n}\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/converters/video-converter.js?");
/***/ }),
/***/ "./src/data/emojiData.js":
/*!*******************************!*\
!*** ./src/data/emojiData.js ***!
\*******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ emojiData: () => (/* binding */ emojiData),\n/* harmony export */ emojiKeywords: () => (/* binding */ emojiKeywords)\n/* harmony export */ });\n// emojiData.js\r\n\r\nconst emojiData = {\r\n smileys: [\r\n // Face smiling\r\n '😀', '😃', '😄', '😆', '😁', '😅', '😂', '🤣', '🥲', '☺️', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😙', '😚', '😗', '😘',\r\n '😋', '🥸', '😵💫',\r\n // Face affection\r\n '😛', '😝', '😜', '🤪', '😎', '🤓', '🧐', '🤨', '🤩', '🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣', '😖', '😫',\r\n // Face tongue\r\n '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫',\r\n // Face negative\r\n '🤥', '😶', '😐', '😑', '😬', '🙄', '😯', '😦', '😧', '😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐', '🥴', '🤢', '🤮', '🤧',\r\n // Face costume\r\n '😷', '🤒', '🤕', '🤑', '🤠', '😈', '👿', '👹', '👺', '🤡', '💩', '👻', '💀', '☠️', '👽', '👾', '🤖', '🎃',\r\n // Cat faces\r\n '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾',\r\n // Hand gestures\r\n '👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎',\r\n // Hand symbols\r\n '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏',\r\n // Body parts\r\n '✍️', '💅', '🤳', '💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀', '🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄',\r\n // Person\r\n '👶', '🧒', '👦', '👧', '🧑', '👱', '👨', '🧔', '👩', '🧓', '👴', '👵', '🙍', '🙎', '🙅', '🙆', '💁', '🙋', '🧏', '🙇', '🤦', '🤷',\r\n // Professional\r\n '👮', '🕵️', '💂', '🥷', '👷', '🤴', '👸', '👳', '👲', '🧕', '🤵', '👰', '🤰', '🤱', '👼', '🎅', '🤶', '🦸', '🦹', '🧙', '🧚', '🧛', '🧜'\r\n ],\r\n\r\n nature: [\r\n // Mammals\r\n '🐵', '🐒', '🦍', '🦧', '🐶', '🐕', '🦮', '🐕🦺', '🐩', '🐺', '🦊', '🦝', '🐱', '🐈', '🐈⬛', '🦁', '🐯', '🐅', '🐆', '🐴', '🐎',\r\n '🦄', '🦓', '🦌', '🦬', '🐮', '🐂', '🐃', '🐄', '🐷', '🐖', '🐗', '🐽', '🐏', '🐑', '🐐', '🐪', '🐫', '🦙', '🦒', '🐘', '🦏',\r\n '🦛', '🐭', '🐁', '🐀', '🐹', '🐰', '🐇', '🦫', '🦘', '🦡', '🐿️', '🦔', '🦦', '🦥', '🐼', '🦨', '🦘', '🦡',\r\n // Birds\r\n '🦃', '🐔', '🐓', '🐣', '🐤', '🐥', '🐦', '🐧', '🕊️', '🦅', '🦆', '🦢', '🦉', '🦤', '🪶', '🦩', '🦚', '🦜',\r\n // Reptiles/Amphibians\r\n '🐸', '🐊', '🐢', '🦎', '🐍', '🐲', '🐉', '🦕', '🦖',\r\n // Marine\r\n '🐳', '🐋', '🐬', '🦭', '🐟', '🐠', '🐡', '🦈', '🐙', '🐚', '🪸',\r\n // Insects\r\n '🐌', '🦋', '🐛', '🐜', '🐝', '🪲', '🐞', '🦗', '🪳', '🕷️', '🕸️', '🦂', '🦟', '🪰', '🪱',\r\n // Plants\r\n '🌸', '💮', '🏵️', '🌹', '🥀', '🌺', '🌻', '🌼', '🌷', '🌱', '🪴', '🌲', '🌳', '🌴', '🌵', '🌾', '🌿', '☘️', '🍀', '🍁', '🍂', '🍃'\r\n ],\r\n\r\n food: [\r\n // Fruits\r\n '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝',\r\n // Vegetables\r\n '🍅', '🍆', '🥑', '🥦', '🥬', '🥒', '🌶️', '🫑', '🥕', '🧄', '🧅', '🥔', '🍠', '🥐', '🥯', '🍞', '🥖', '🥨',\r\n // Prepared\r\n '🧀', '🥚', '🍳', '🥓', '🥩', '🍗', '🍖', '🦴', '🌭', '🍔', '🍟', '🍕', '🫓', '🥪', '🥙', '🧆', '🌮', '🌯', '🫔', '🥗',\r\n // Asian\r\n '🥘', '🫕', '🥫', '🍝', '🍜', '🍲', '🍛', '🍣', '🍱', '🥟', '🦪', '🍤', '🍙', '🍚', '🍘', '🍥', '🥠', '🥮',\r\n // Sweets\r\n '🍢', '🍡', '🍧', '🍨', '🍦', '🥧', '🧁', '🍰', '🎂', '🍮', '🍭', '🍬', '🍫', '🍿', '🍩', '🍪',\r\n // Drink\r\n '🫖', '☕', '🍵', '🧃', '🥤', '🧋', '🍶', '🍺', '🍻', '🥂', '🍷', '🥃', '🍸', '🍹', '🧉', '🍾'\r\n ],\r\n\r\n activities: [\r\n // Sports\r\n '⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', '⛳', '🪁', '🎣',\r\n '🤿', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌', '⛷️', '🏂', '🪂', '🏋️', '🤼', '🤸', '⛹️', '🤾', '🏌️', '🏇', '🧘', '🏄', '🏊',\r\n // Activities\r\n '🤽', '🚣', '🧗', '🚴', '🚵', '🎪', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸', '🎻', '🎲', '🎯',\r\n '🎳', '🎮', '🎰', '🧩', '🎪', '🎫', '🎟️'\r\n ],\r\n\r\n travel: [\r\n // Land transport\r\n '🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🛻', '🚚', '🚛', '🚜', '🛵', '🏍️', '🛺', '🚲', '🛴',\r\n // Air transport\r\n '✈️', '🛩️', '🛫', '🛬', '🚁', '🚀', '🛸',\r\n // Water transport\r\n '🛶', '⛵', '🚤', '🛥️', '🛳️', '⛴️', '🚢',\r\n // Places\r\n '🏰', '🏯', '🏟️', '🏖️', '🏝️', '🏜️', '🌋', '⛰️', '🏔️', '🗻', '🏕️', '🏭', '🏢', '🏬', '🏣', '🏤', '🏥', '🏦', '🏨',\r\n '🏪', '🏫', '🏩', '💒', '⛪', '🕌', '🕍', '🛕', '⛩️', '🏛️'\r\n ],\r\n\r\n objects: [\r\n // Tools\r\n '📱', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️', '🕹️', '🗜️', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥',\r\n // Office\r\n '📞', '☎️', '📟', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '📡', '🔋', '🔌', '💡', '🔦', '🕯️',\r\n // Household\r\n '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '🪙', '💰', '💳', '💎', '⚖️', '🪜', '🧰', '🔧', '🔨', '⚒️', '🛠️', '⛏️',\r\n // Writing\r\n '✏️', '🖊️', '🖋️', '✒️', '🖌️', '🖍️', '📝', '📚', '📖', '🔖', '📑', '🗒️', '📄', '📰', '🗞️', '📁', '📂', '🗂️',\r\n // Clocks (additional clock emojis)\r\n '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛', '🕰️', '⏰', '⏱️', '⏲️', '⌚'\r\n ],\r\n\r\n symbols: [\r\n // Hearts\r\n '❤️', '🧡', '💛', '💚', '💙', '💜', '🤎', '🖤', '🤍', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟',\r\n // Religion\r\n '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎',\r\n // Warning\r\n '⚠️', '🚸', '⛔', '🚫', '☢️', '☣️',\r\n // Math\r\n '➕', '➖', '➗', '✖️', '♾️', '💲', '💱',\r\n // Arrows\r\n '⬆️', '↗️', '➡️', '↘️', '⬇️', '↙️', '⬅️', '↖️', '↕️', '↔️', '↩️', '↪️', '⤴️', '⤵️', '🔃', '🔄',\r\n // Other\r\n '🔆', '📶', '🎦', '🔅', '♻️', '✅', '❌', '❎', '➰', '➿', '〽️', '✳️', '✴️', '❇️', '©️', '®️', '™️'\r\n ],\r\n\r\n flags: [\r\n // Special flags\r\n '🏁', '🚩', '🎌', '🏴', '🏳️', '🏳️🌈', '🏳️⚧️', '🏴☠️',\r\n // Country flags (sample)\r\n '🇺🇸', '🇬🇧', '🇯🇵', '🇰🇷', '🇩🇪', '🇨🇳', '🇧🇷', '🇮🇳', '🇫🇷', '🇪🇸', '🇮🇹', '🇷🇺', '🇨🇦', '🇦🇺', '🇳🇿',\r\n // More popular country flags\r\n '🇲🇽', '🇦🇷', '🇵🇰', '🇪🇬', '🇸🇪', '🇳🇴', '🇳🇱', '🇨🇭', '🇹🇷', '🇮🇩', '🇸🇬', '🇮🇱', '🇵🇹', '🇵🇱', '🇹🇭'\r\n ]\r\n};\r\n\r\nconst emojiKeywords = {\r\n //------------------------- Smileys & Emotion -------------------------\r\n // Face smiling\r\n '😀': {\r\n en: ['grinning', 'face', 'smile', 'happy', 'joy'],\r\n ru: ['улыбающийся', 'лицо', 'улыбка', 'счастливый', 'радость']\r\n },\r\n '😃': {\r\n en: ['smiley', 'face', 'happy', 'joy', 'laugh'],\r\n ru: ['улыбчивый', 'лицо', 'счастливый', 'радость', 'смех']\r\n },\r\n '😄': {\r\n en: ['laughing', 'face', 'happy', 'joy', 'grin'],\r\n ru: ['смеющийся', 'лицо', 'счастливый', 'радость', 'ухмылка']\r\n },\r\n '😆': {\r\n en: ['grinning face', 'laugh'],\r\n ru: ['улыбка', 'смех']\r\n },\r\n '😁': {\r\n en: ['beaming', 'face', 'grin', 'smile', 'happy'],\r\n ru: ['сияющий', 'лицо', 'улыбка', 'счастье', 'радость']\r\n },\r\n '😅': {\r\n en: ['sweat', 'nervous', 'face', 'laugh', 'relief'],\r\n ru: ['пот', 'нервный', 'лицо', 'смех', 'облегчение']\r\n },\r\n '😂': {\r\n en: ['tears', 'joy', 'face', 'laugh', 'happy'],\r\n ru: ['слёзы', 'радость', 'лицо', 'смех', 'счастье']\r\n },\r\n '🤣': {\r\n en: ['rolling', 'floor', 'laugh', 'funny', 'amused'],\r\n ru: ['катающийся', 'пол', 'смех', 'смешной', 'развлечённый']\r\n },\r\n '🥲': {\r\n en: ['smiling', 'tear', 'bittersweet', 'nostalgic', 'happy'],\r\n ru: ['улыбающийся', 'слеза', 'горько-сладкий', 'ностальгический', 'счастливый']\r\n },\r\n '☺️': {\r\n en: ['smile', 'blush', 'content', 'peaceful'],\r\n ru: ['улыбка', 'румянец', 'довольный', 'спокойный']\r\n },\r\n '😊': {\r\n en: ['smiling', 'happy', 'blushing', 'content'],\r\n ru: ['улыбающийся', 'счастливый', 'румянец', 'довольный']\r\n },\r\n '😇': {\r\n en: ['angel', 'halo', 'innocent', 'saint', 'pure'],\r\n ru: ['ангел', 'ореол', 'невинный', 'святой', 'чистый']\r\n },\r\n '🙂': {\r\n en: ['slight', 'smile', 'face', 'mild'],\r\n ru: ['слегка', 'улыбка', 'лицо', 'умеренный']\r\n },\r\n '🙃': {\r\n en: ['upside-down', 'silly', 'quirky', 'funny'],\r\n ru: ['перевернутый', 'глупый', 'странный', 'смешной']\r\n },\r\n '😉': {\r\n en: ['wink', 'flirt', 'playful', 'smile'],\r\n ru: ['подмигивание', 'флирт', 'игривый', 'улыбка']\r\n },\r\n '😌': {\r\n en: ['relieved', 'calm', 'content', 'satisfied'],\r\n ru: ['облегчённый', 'спокойный', 'довольный', 'удовлетворённый']\r\n },\r\n '😍': {\r\n en: ['heart', 'love', 'smiling', 'eyes', 'adore'],\r\n ru: ['сердце', 'любовь', 'улыбающийся', 'глаза', 'обожать']\r\n },\r\n '🥰': {\r\n en: ['love', 'hearts', 'affection', 'adoration', 'cuddle'],\r\n ru: ['любовь', 'сердца', 'нежность', 'обожание', 'обниматься']\r\n },\r\n '😙': {\r\n en: ['kiss', 'love', 'affection', 'flirt'],\r\n ru: ['поцелуй', 'любовь', 'нежность', 'флирт']\r\n },\r\n '😗': {\r\n en: ['kiss', 'face', 'smile', 'affection'],\r\n ru: ['поцелуй', 'лицо', 'улыбка', 'нежность']\r\n },\r\n '😚': {\r\n en: ['kiss', 'closed eyes', 'affection', 'love'],\r\n ru: ['поцелуй', 'закрытые глаза', 'нежность', 'любовь']\r\n },\r\n '😘': {\r\n en: ['kiss', 'love', 'affection', 'flirt'],\r\n ru: ['поцелуй', 'любовь', 'нежность', 'флирт']\r\n },\r\n '😋': {\r\n en: ['yum', 'delicious', 'tasty', 'savor', 'lick'],\r\n ru: ['ням', 'вкусно', 'аппетитно', 'наслаждаться', 'лизать']\r\n },\r\n '🥸': {\r\n en: ['disguise', 'glasses', 'funny', 'sneaky', 'face'],\r\n ru: ['маскировка', 'очки', 'смешной', 'хитрый', 'лицо']\r\n },\r\n '😵💫': {\r\n en: ['dizzy', 'spiral eyes', 'confused', 'hypnotized', 'disoriented'],\r\n ru: ['головокружение', 'спиральные глаза', 'запутанный', 'загипнотизированный', 'дезориентированный']\r\n },\r\n\r\n // Face affection\r\n '😛': {\r\n en: ['tongue', 'playful', 'cheeky', 'silly'],\r\n ru: ['язык', 'игривый', 'нахальный', 'глупый']\r\n },\r\n '😝': {\r\n en: ['tongue', 'silly', 'wacky', 'fun'],\r\n ru: ['язык', 'глупый', 'безумный', 'весёлый']\r\n },\r\n '😜': {\r\n en: ['tongue', 'wink', 'playful', 'fun'],\r\n ru: ['язык', 'подмигивание', 'игривый', 'весёлый']\r\n },\r\n '🤪': {\r\n en: ['crazy', 'wacky', 'zany', 'quirky'],\r\n ru: ['сумасшедший', 'безумный', 'чудаковатый', 'странный']\r\n },\r\n '😎': {\r\n en: ['cool', 'sunglasses', 'confident', 'chill'],\r\n ru: ['крутой', 'очки', 'уверенный', 'расслабленный']\r\n },\r\n '🤓': {\r\n en: ['nerd', 'geek', 'glasses', 'studious'],\r\n ru: ['ботан', 'задрот', 'очки', 'учёный']\r\n },\r\n '🧐': {\r\n en: ['monocle', 'investigative', 'curious', 'thoughtful'],\r\n ru: ['одноглазый', 'расследовательский', 'любопытный', 'задумчивый']\r\n },\r\n '🤨': {\r\n en: ['skeptical', 'doubtful', 'uncertain', 'raised eyebrow'],\r\n ru: ['скептический', 'сомневающийся', 'неуверенный', 'поднятая бровь']\r\n },\r\n '🤩': {\r\n en: ['starstruck', 'amazed', 'excited', 'admire'],\r\n ru: ['восхищённый', 'поражённый', 'взволнованный', 'обожать']\r\n },\r\n '🥳': {\r\n en: ['party', 'celebrate', 'birthday', 'festive'],\r\n ru: ['вечеринка', 'праздновать', 'день рождения', 'праздничный']\r\n },\r\n '😏': {\r\n en: ['smirk', 'sly', 'mischievous', 'confident'],\r\n ru: ['ухмылка', 'хитрый', 'озорной', 'уверенный']\r\n },\r\n '😒': {\r\n en: ['unamused', 'displeased', 'bored', 'sigh'],\r\n ru: ['неудовлетворённый', 'недовольный', 'скучный', 'вздох']\r\n },\r\n '😞': {\r\n en: ['disappointed', 'sad', 'down', 'somber'],\r\n ru: ['разочарованный', 'грустный', 'унылый', 'мрачный']\r\n },\r\n '😔': {\r\n en: ['pensive', 'sad', 'reflective', 'mournful'],\r\n ru: ['задумчивый', 'грустный', 'рефлексивный', 'скорбный']\r\n },\r\n '😟': {\r\n en: ['worried', 'concerned', 'anxious', 'upset'],\r\n ru: ['обеспокоенный', 'тревожный', 'беспокойный', 'расстроенный']\r\n },\r\n '😕': {\r\n en: ['confused', 'perplexed', 'uncertain', 'baffled'],\r\n ru: ['смущённый', 'озадаченный', 'неуверенный', 'в замешательстве']\r\n },\r\n '🙁': {\r\n en: ['frowning', 'sad', 'disappointed', 'downcast'],\r\n ru: ['хмурый', 'грустный', 'разочарованный', 'угнетённый']\r\n },\r\n '☹️': {\r\n en: ['frowning', 'sad', 'unhappy', 'mournful'],\r\n ru: ['хмурый', 'грустный', 'несчастный', 'скорбный']\r\n },\r\n '😣': {\r\n en: ['strained', 'persevering', 'tired', 'discomfort'],\r\n ru: ['напряжённый', 'терпеливый', 'уставший', 'дискомфорт']\r\n },\r\n '😖': {\r\n en: ['confounded', 'annoyed', 'distressed', 'exasperated'],\r\n ru: ['озадаченный', 'раздражённый', 'огорчённый', 'изнурённый']\r\n },\r\n '😫': {\r\n en: ['tired', 'exhausted', 'weary', 'worn out'],\r\n ru: ['усталый', 'измученный', 'изнурённый', 'измотанный']\r\n },\r\n\r\n // Face tongue\r\n '😩': {\r\n en: ['weary', 'tired', 'exhausted', 'overwhelmed'],\r\n ru: ['изнурённый', 'уставший', 'измученный', 'перегруженный']\r\n },\r\n '🥺': {\r\n en: ['pleading', 'begging', 'cute', 'vulnerable'],\r\n ru: ['умоляющий', 'просьба', 'милый', 'уязвимый']\r\n },\r\n '😢': {\r\n en: ['cry', 'sad', 'tear', 'sorrow'],\r\n ru: ['плакать', 'грустный', 'слеза', 'печаль']\r\n },\r\n '😭': {\r\n en: ['crying', 'tearful', 'sad', 'heartbroken'],\r\n ru: ['плачущий', 'со слезами', 'грустный', 'с разбитым сердцем']\r\n },\r\n '😤': {\r\n en: ['triumphant', 'exasperated', 'proud', 'angry'],\r\n ru: ['триумфальный', 'раздражённый', 'гордый', 'сердитый']\r\n },\r\n '😠': {\r\n en: ['angry', 'mad', 'annoyed', 'irate'],\r\n ru: ['сердитый', 'злой', 'раздражённый', 'яростный']\r\n },\r\n '😡': {\r\n en: ['pouting', 'mad', 'furious', 'irate'],\r\n ru: ['надувшийся', 'злой', 'яростный', 'взбешённый']\r\n },\r\n '🤬': {\r\n en: ['cursing', 'swearing', 'angry', 'foul language'],\r\n ru: ['ругательства', 'мат', 'сердитый', 'неприличный']\r\n },\r\n '🤯': {\r\n en: ['mind blown', 'shocked', 'amazed', 'stunned'],\r\n ru: ['взрыв мозга', 'шокированный', 'поражённый', 'ошеломлённый']\r\n },\r\n '😳': {\r\n en: ['flushed', 'embarrassed', 'shocked', 'awkward'],\r\n ru: ['покрасневший', 'смущённый', 'шокированный', 'неловкий']\r\n },\r\n '🥵': {\r\n en: ['hot', 'overheated', 'sweaty', 'exhausted'],\r\n ru: ['горячий', 'перегретый', 'потный', 'изнурённый']\r\n },\r\n '🥶': {\r\n en: ['cold', 'freezing', 'chilly', 'frozen'],\r\n ru: ['холодный', 'замерзающий', 'прохладный', 'замороженный']\r\n },\r\n '😱': {\r\n en: ['screaming', 'horror', 'shock', 'fear'],\r\n ru: ['кричащий', 'ужас', 'шок', 'страх']\r\n },\r\n '😨': {\r\n en: ['fearful', 'scared', 'anxious', 'nervous'],\r\n ru: ['боязливый', 'испуганный', 'тревожный', 'нервный']\r\n },\r\n '😰': {\r\n en: ['anxious', 'nervous', 'sweating', 'scared'],\r\n ru: ['тревожный', 'нервный', 'потеющий', 'испуганный']\r\n },\r\n '😥': {\r\n en: ['disappointed', 'sad', 'pensive', 'teary'],\r\n ru: ['разочарованный', 'грустный', 'задумчивый', 'со слезами']\r\n },\r\n '😓': {\r\n en: ['cold sweat', 'nervous', 'anxious', 'tired'],\r\n ru: ['холодный пот', 'нервный', 'тревожный', 'усталый']\r\n },\r\n '🤗': {\r\n en: ['hug', 'embrace', 'caring', 'love'],\r\n ru: ['объятие', 'принимать', 'заботливый', 'любовь']\r\n },\r\n '🤔': {\r\n en: ['thinking', 'pondering', 'curious', 'confused'],\r\n ru: ['думающий', 'размышляющий', 'любопытный', 'смущённый']\r\n },\r\n '🤭': {\r\n en: ['guilty', 'shy', 'embarrassed', 'oops'],\r\n ru: ['виноватый', 'застенчивый', 'смущённый', 'упс']\r\n },\r\n '🤫': {\r\n en: ['quiet', 'secret', 'hush', 'shh'],\r\n ru: ['тихий', 'секрет', 'тишина', 'ш-ш']\r\n },\r\n\r\n // Face negative\r\n '🤥': {\r\n en: ['lying', 'fib', 'deceitful', 'dishonest'],\r\n ru: ['врущий', 'ложь', 'обманчивый', 'нечестный']\r\n },\r\n '😶': {\r\n en: ['speechless', 'mute', 'quiet', 'blank'],\r\n ru: ['безмолвный', 'немой', 'тихий', 'пустой']\r\n },\r\n '😐': {\r\n en: ['neutral', 'expressionless', 'indifferent', 'flat'],\r\n ru: ['нейтральный', 'без выражения', 'безразличный', 'плоский']\r\n },\r\n '😑': {\r\n en: ['deadpan', 'expressionless', 'blank', 'unemotional'],\r\n ru: ['без эмоций', 'без выражения', 'пустой', 'неэмоциональный']\r\n },\r\n '😬': {\r\n en: ['grimace', 'awkward', 'nervous', 'tense'],\r\n ru: ['гримаса', 'неловко', 'нервный', 'напряжённый']\r\n },\r\n '🙄': {\r\n en: ['eye roll', 'sarcastic', 'disdain', 'bored'],\r\n ru: ['закатывание глаз', 'саркастичный', 'пренебрежительный', 'скучный']\r\n },\r\n '😯': {\r\n en: ['hushed', 'surprised', 'shocked', 'amazed'],\r\n ru: ['тихий', 'удивлённый', 'шокированный', 'поражённый']\r\n },\r\n '😦': {\r\n en: ['frowning', 'dismayed', 'shocked', 'surprised'],\r\n ru: ['хмурый', 'огорчённый', 'шокированный', 'удивлённый']\r\n },\r\n '😧': {\r\n en: ['astonished', 'stunned', 'surprised', 'speechless'],\r\n ru: ['изумлённый', 'ошеломлённый', 'удивлённый', 'безмолвный']\r\n },\r\n '😮': {\r\n en: ['open mouth', 'surprised', 'shocked', 'amazed'],\r\n ru: ['открытый рот', 'удивлённый', 'шокированный', 'поражённый']\r\n },\r\n '😲': {\r\n en: ['astonished', 'stunned', 'shocked', 'in awe'],\r\n ru: ['изумлённый', 'ошеломлённый', 'шокированный', 'в благоговении']\r\n },\r\n '🥱': {\r\n en: ['yawning', 'sleepy', 'tired', 'bored'],\r\n ru: ['зевота', 'сонный', 'усталый', 'скучный']\r\n },\r\n '😴': {\r\n en: ['sleeping', 'tired', 'napping', 'dozing'],\r\n ru: ['спящий', 'усталый', 'дремлющий', 'засыпающий']\r\n },\r\n '🤤': {\r\n en: ['drooling', 'desire', 'craving', 'hungry'],\r\n ru: ['слюнявый', 'желание', 'тяга', 'голодный']\r\n },\r\n '😪': {\r\n en: ['sleepy', 'drowsy', 'tired', 'nodding'],\r\n ru: ['сонный', 'вялый', 'усталый', 'кивающий']\r\n },\r\n '😵': {\r\n en: ['dizzy', 'knocked out', 'stunned', 'confused'],\r\n ru: ['головокружительный', 'вырубленный', 'ошеломлённый', 'сбитый с толку']\r\n },\r\n '🤐': {\r\n en: ['zipper-mouth', 'secretive', 'quiet', 'mute'],\r\n ru: ['закрытый рот', 'секретный', 'тихий', 'немой']\r\n },\r\n '🥴': {\r\n en: ['woozy', 'tipsy', 'dizzy', 'unsteady'],\r\n ru: ['ошеломлённый', 'подошедший', 'головокружительный', 'неустойчивый']\r\n },\r\n '🤢': {\r\n en: ['nauseated', 'sick', 'disgusted', 'vomit'],\r\n ru: ['тошнотворный', 'больной', 'отвратительный', 'рвота']\r\n },\r\n '🤮': {\r\n en: ['vomiting', 'nauseous', 'sick', 'disgust'],\r\n ru: ['рвота', 'тошнотворный', 'больной', 'отвратительный']\r\n },\r\n '🤧': {\r\n en: ['sneezing', 'ill', 'sick', 'allergy'],\r\n ru: ['чихание', 'болен', 'больной', 'аллергия']\r\n },\r\n\r\n // Face costume\r\n '😷': {\r\n en: ['mask', 'sick', 'ill', 'health'],\r\n ru: ['маска', 'больной', 'нездоровый', 'здоровье']\r\n },\r\n '🤒': {\r\n en: ['fever', 'sick', 'ill', 'unwell'],\r\n ru: ['лихорадка', 'больной', 'нездоровый', 'неважно']\r\n },\r\n '🤕': {\r\n en: ['injured', 'hurt', 'bandaged', 'pain'],\r\n ru: ['раненый', 'повреждённый', 'забинтованный', 'боль']\r\n },\r\n '🤑': {\r\n en: ['money', 'rich', 'greedy', 'cash'],\r\n ru: ['деньги', 'богатый', 'жадный', 'наличные']\r\n },\r\n '🤠': {\r\n en: ['cowboy', 'hat', 'western', 'fun'],\r\n ru: ['ковбой', 'шляпа', 'вестерн', 'веселье']\r\n },\r\n '😈': {\r\n en: ['devil', 'mischievous', 'naughty', 'sinister'],\r\n ru: ['дьявол', 'озорной', 'непослушный', 'зловещий']\r\n },\r\n '👿': {\r\n en: ['angry', 'devil', 'evil', 'fiendish'],\r\n ru: ['сердитый', 'дьявол', 'злой', 'зловещий']\r\n },\r\n '👹': {\r\n en: ['ogre', 'demon', 'monster', 'scary'],\r\n ru: ['огр', 'демон', 'монстр', 'страшный']\r\n },\r\n '👺': {\r\n en: ['goblin', 'troll', 'spooky', 'creepy'],\r\n ru: ['гоблин', 'тролль', 'жуткий', 'страшный']\r\n },\r\n '🤡': {\r\n en: ['clown', 'silly', 'funny', 'circus'],\r\n ru: ['клоун', 'глупый', 'смешной', 'цирк']\r\n },\r\n '💩': {\r\n en: ['poop', 'crap', 'feces', 'funny'],\r\n ru: ['какашка', 'дерьмо', 'фекалии', 'смешной']\r\n },\r\n '👻': {\r\n en: ['ghost', 'spirit', 'haunted', 'scary'],\r\n ru: ['призрак', 'дух', 'обитающий', 'страшный']\r\n },\r\n '💀': {\r\n en: ['skull', 'death', 'creepy', 'spooky'],\r\n ru: ['череп', 'смерть', 'жуткий', 'страшный']\r\n },\r\n '☠️': {\r\n en: ['skull', 'danger', 'death', 'poison'],\r\n ru: ['череп', 'опасность', 'смерть', 'яд']\r\n },\r\n '👽': {\r\n en: ['alien', 'extraterrestrial', 'space', 'ufo'],\r\n ru: ['инопланетянин', 'внеземной', 'космос', 'НЛО']\r\n },\r\n '👾': {\r\n en: ['alien', 'monster', 'video game', 'retro'],\r\n ru: ['инопланетянин', 'монстр', 'видеоигра', 'ретро']\r\n },\r\n '🤖': {\r\n en: ['robot', 'machine', 'tech', 'android'],\r\n ru: ['робот', 'машина', 'технология', 'андроид']\r\n },\r\n '🎃': {\r\n en: ['pumpkin', 'halloween', 'spooky', 'festive'],\r\n ru: ['тыква', 'Хэллоуин', 'жуткий', 'праздничный']\r\n },\r\n\r\n // Cat faces\r\n '😺': {\r\n en: ['smiling', 'cat', 'happy', 'playful'],\r\n ru: ['улыбающийся', 'кот', 'счастливый', 'игривый']\r\n },\r\n '😸': {\r\n en: ['grinning', 'cat', 'joyful', 'cheerful'],\r\n ru: ['широко улыбающийся', 'кот', 'радостный', 'весёлый']\r\n },\r\n '😹': {\r\n en: ['tearful', 'joy', 'cat', 'laughing'],\r\n ru: ['со слезами', 'радость', 'кот', 'смеющийся']\r\n },\r\n '😻': {\r\n en: ['heart', 'cat', 'love', 'adorable'],\r\n ru: ['сердце', 'кот', 'любовь', 'милый']\r\n },\r\n '😼': {\r\n en: ['smirking', 'cat', 'mischievous', 'sly'],\r\n ru: ['ухмыляющийся', 'кот', 'озорной', 'хитрый']\r\n },\r\n '😽': {\r\n en: ['kissing', 'cat', 'affection', 'cute'],\r\n ru: ['целующий', 'кот', 'нежность', 'милый']\r\n },\r\n '🙀': {\r\n en: ['surprised', 'cat', 'scared', 'shocked'],\r\n ru: ['испуганный', 'кот', 'испуганный', 'шокированный']\r\n },\r\n '😿': {\r\n en: ['crying', 'cat', 'sad', 'tearful'],\r\n ru: ['плачущий', 'кот', 'грустный', 'со слезами']\r\n },\r\n '😾': {\r\n en: ['angry', 'cat', 'annoyed', 'displeased'],\r\n ru: ['сердитый', 'кот', 'раздражённый', 'недовольный']\r\n },\r\n\r\n //------------------------- People & Body -------------------------\r\n // Hand gestures\r\n '👋': {\r\n en: ['wave', 'hello', 'goodbye', 'greeting'],\r\n ru: ['махание', 'привет', 'прощание', 'приветствие']\r\n },\r\n '🤚': {\r\n en: ['raised hand', 'stop', 'palm'],\r\n ru: ['поднятая рука', 'стой', 'ладонь']\r\n },\r\n '🖐️': {\r\n en: ['hand', 'high five', 'greeting'],\r\n ru: ['рука', 'дай пять', 'приветствие']\r\n },\r\n '✋': {\r\n en: ['stop', 'palm', 'high five'],\r\n ru: ['стой', 'ладонь', 'дай пять']\r\n },\r\n '🖖': {\r\n en: ['vulcan salute', 'live long', 'sci-fi'],\r\n ru: ['салют Вулканцев', 'живи долго', 'научная фантастика']\r\n },\r\n '👌': {\r\n en: ['okay', 'perfect', 'good'],\r\n ru: ['ок', 'идеально', 'хорошо']\r\n },\r\n '🤌': {\r\n en: ['pinched', 'precise', 'delicious'],\r\n ru: ['сжатый', 'точный', 'вкусный']\r\n },\r\n '🤏': {\r\n en: ['small', 'tiny', 'minuscule'],\r\n ru: ['маленький', 'крошечный', 'микроскопический']\r\n },\r\n '✌️': {\r\n en: ['peace', 'victory', 'v sign'],\r\n ru: ['мир', 'победа', 'знак победы']\r\n },\r\n '🤞': {\r\n en: ['fingers crossed', 'hope', 'luck'],\r\n ru: ['скрещенные пальцы', 'надежда', 'удача']\r\n },\r\n '🤟': {\r\n en: ['I love you', 'rock on', 'sign language'],\r\n ru: ['я тебя люблю', 'рок он', 'язык жестов']\r\n },\r\n '🤘': {\r\n en: ['rock', 'metal', 'horns'],\r\n ru: ['рок', 'металл', 'рога']\r\n },\r\n '🤙': {\r\n en: ['call me', 'hang loose', 'shaka'],\r\n ru: ['позвони мне', 'расслабься', 'шак']\r\n },\r\n '👈': {\r\n en: ['point left', 'direction', 'arrow'],\r\n ru: ['указание влево', 'направление', 'стрелка']\r\n },\r\n '👉': {\r\n en: ['point right', 'direction', 'arrow'],\r\n ru: ['указание вправо', 'направление', 'стрелка']\r\n },\r\n '👆': {\r\n en: ['point up', 'direction', 'up'],\r\n ru: ['указание вверх', 'направление', 'вверх']\r\n },\r\n '🖕': {\r\n en: ['middle finger', 'offensive', 'rude'],\r\n ru: ['средний палец', 'оскорбительный', 'грубый']\r\n },\r\n '👇': {\r\n en: ['point down', 'direction', 'down'],\r\n ru: ['указание вниз', 'направление', 'вниз']\r\n },\r\n '☝️': {\r\n en: ['point up', 'number one', 'important'],\r\n ru: ['указание вверх', 'номер один', 'важный']\r\n },\r\n '👍': {\r\n en: ['thumbs up', 'good', 'approve'],\r\n ru: ['палец вверх', 'хорошо', 'одобрить']\r\n },\r\n '👎': {\r\n en: ['thumbs down', 'bad', 'disapprove'],\r\n ru: ['палец вниз', 'плохо', 'не одобрять']\r\n },\r\n\r\n // Hand symbols\r\n '✊': {\r\n en: ['fist', 'power', 'solidarity'],\r\n ru: ['кулак', 'сила', 'солидарность']\r\n },\r\n '👊': {\r\n en: ['punch', 'fist bump', 'hit'],\r\n ru: ['удар', 'кулачок', 'ударить']\r\n },\r\n '🤛': {\r\n en: ['left fist', 'punch', 'strike'],\r\n ru: ['левая рука', 'удар', 'нанести удар']\r\n },\r\n '🤜': {\r\n en: ['right fist', 'punch', 'strike'],\r\n ru: ['правая рука', 'удар', 'нанести удар']\r\n },\r\n '👏': {\r\n en: ['clap', 'applause', 'bravo'],\r\n ru: ['хлопки', 'аплодисменты', 'браво']\r\n },\r\n '🙌': {\r\n en: ['celebrate', 'praise', 'hooray'],\r\n ru: ['торжествовать', 'хвалить', 'ура']\r\n },\r\n '👐': {\r\n en: ['open hands', 'embrace', 'welcome'],\r\n ru: ['раскрытые руки', 'объятие', 'добро пожаловать']\r\n },\r\n '🤲': {\r\n en: ['palms', 'offering', 'receive'],\r\n ru: ['ладони', 'предложение', 'получать']\r\n },\r\n '🤝': {\r\n en: ['handshake', 'agreement', 'cooperation'],\r\n ru: ['рукопожатие', 'соглашение', 'сотрудничество']\r\n },\r\n '🙏': {\r\n en: ['pray', 'thanks', 'please'],\r\n ru: ['молиться', 'спасибо', 'пожалуйста']\r\n },\r\n\r\n // Body parts\r\n '✍️': {\r\n en: ['writing', 'pen', 'signature'],\r\n ru: ['письмо', 'ручка', 'подпись']\r\n },\r\n '💅': {\r\n en: ['nail polish', 'beauty', 'manicure'],\r\n ru: ['лак для ногтей', 'красота', 'маникюр']\r\n },\r\n '🤳': {\r\n en: ['selfie', 'photo', 'camera'],\r\n ru: ['селфи', 'фото', 'камера']\r\n },\r\n '💪': {\r\n en: ['flex', 'strong', 'muscle'],\r\n ru: ['сгибать', 'сильный', 'мышцы']\r\n },\r\n '🦾': {\r\n en: ['mechanical arm', 'robotic', 'cyborg'],\r\n ru: ['механическая рука', 'роботизированный', 'киборг']\r\n },\r\n '🦿': {\r\n en: ['mechanical leg', 'prosthetic', 'robotic'],\r\n ru: ['механическая нога', 'протез', 'роботизированный']\r\n },\r\n '🦵': {\r\n en: ['leg', 'limb', 'lower body'],\r\n ru: ['нога', 'конечность', 'нижняя часть тела']\r\n },\r\n '🦶': {\r\n en: ['foot', 'toes', 'step'],\r\n ru: ['нога', 'пальцы ноги', 'шаг']\r\n },\r\n '👂': {\r\n en: ['ear', 'listening', 'sound'],\r\n ru: ['ухо', 'слушание', 'звук']\r\n },\r\n '🦻': {\r\n en: ['hearing aid', 'listening', 'assistive'],\r\n ru: ['слуховой аппарат', 'слушание', 'помощь']\r\n },\r\n '👃': {\r\n en: ['nose', 'smell', 'scent'],\r\n ru: ['нос', 'запах', 'аромат']\r\n },\r\n '🧠': {\r\n en: ['brain', 'intelligence', 'mind'],\r\n ru: ['мозг', 'интеллект', 'ум']\r\n },\r\n '🫀': {\r\n en: ['heart (organ)', 'anatomy', 'biology'],\r\n ru: ['сердце (орган)', 'анатомия', 'биология']\r\n },\r\n '🫁': {\r\n en: ['lungs', 'breath', 'organ'],\r\n ru: ['легкие', 'дыхание', 'орган']\r\n },\r\n '🦷': {\r\n en: ['tooth', 'dental', 'smile'],\r\n ru: ['зуб', 'стоматология', 'улыбка']\r\n },\r\n '🦴': {\r\n en: ['bone', 'skeleton', 'hard'],\r\n ru: ['кость', 'скелет', 'твердый']\r\n },\r\n '👀': {\r\n en: ['eyes', 'look', 'see'],\r\n ru: ['глаза', 'смотреть', 'видеть']\r\n },\r\n '👁️': {\r\n en: ['eye', 'vision', 'watch'],\r\n ru: ['глаз', 'зрение', 'наблюдать']\r\n },\r\n '👅': {\r\n en: ['tongue', 'taste', 'lick'],\r\n ru: ['язык', 'вкус', 'лизать']\r\n },\r\n '👄': {\r\n en: ['lips', 'kiss', 'mouth'],\r\n ru: ['губы', 'поцелуй', 'рот']\r\n },\r\n\r\n // Person\r\n '👶': {\r\n en: ['baby', 'infant', 'cute'],\r\n ru: ['младенец', 'ребенок', 'милый']\r\n },\r\n '🧒': {\r\n en: ['child', 'kid', 'youth'],\r\n ru: ['ребенок', 'малыш', 'юный']\r\n },\r\n '👦': {\r\n en: ['boy', 'child', 'kid'],\r\n ru: ['мальчик', 'ребенок', 'малыш']\r\n },\r\n '👧': {\r\n en: ['girl', 'child', 'kid'],\r\n ru: ['девочка', 'ребенок', 'малышка']\r\n },\r\n '🧑': {\r\n en: ['person', 'human', 'individual'],\r\n ru: ['человек', 'личность', 'индивид']\r\n },\r\n '👱': {\r\n en: ['blonde', 'person', 'light hair'],\r\n ru: ['блондин', 'человек', 'светлые волосы']\r\n },\r\n '👨': {\r\n en: ['man', 'male', 'guy'],\r\n ru: ['мужчина', 'мужской', 'парень']\r\n },\r\n '🧔': {\r\n en: ['bearded', 'man', 'facial hair'],\r\n ru: ['бородатый', 'мужчина', 'борода']\r\n },\r\n '👩': {\r\n en: ['woman', 'female', 'lady'],\r\n ru: ['женщина', 'женский', 'дама']\r\n },\r\n '🧓': {\r\n en: ['elderly', 'senior', 'aged'],\r\n ru: ['пожилой', 'старший', 'в преклонном возрасте']\r\n },\r\n '👴': {\r\n en: ['old man', 'elderly', 'senior'],\r\n ru: ['старик', 'пожилой', 'старший']\r\n },\r\n '👵': {\r\n en: ['old woman', 'elderly', 'senior'],\r\n ru: ['старуха', 'пожилая', 'старшая']\r\n },\r\n '🙍': {\r\n en: ['frowning', 'sad', 'displeased'],\r\n ru: ['хмурый', 'грустный', 'недовольный']\r\n },\r\n '🙎': {\r\n en: ['pouting', 'angry', 'displeased'],\r\n ru: ['надувшийся', 'сердитый', 'недовольный']\r\n },\r\n '🙅': {\r\n en: ['no', 'prohibited', 'refusal'],\r\n ru: ['нет', 'запрещено', 'отказ']\r\n },\r\n '🙆': {\r\n en: ['ok', 'acceptable', 'okay'],\r\n ru: ['ок', 'приемлемо', 'хорошо']\r\n },\r\n '💁': {\r\n en: ['information', 'help', 'assistance'],\r\n ru: ['информация', 'помощь', 'поддержка']\r\n },\r\n '🙋': {\r\n en: ['raising hand', 'question', 'volunteer'],\r\n ru: ['поднимающая руку', 'вопрос', 'доброволец']\r\n },\r\n '🧏': {\r\n en: ['deaf', 'listening', 'silent'],\r\n ru: ['глухой', 'слушающий', 'безмолвный']\r\n },\r\n '🙇': {\r\n en: ['bowing', 'apologetic', 'respect'],\r\n ru: ['наклон', 'извиняющийся', 'уважение']\r\n },\r\n '🤦': {\r\n en: ['facepalm', 'disbelief', 'oops'],\r\n ru: ['лицо ладонь', 'недоверие', 'упс']\r\n },\r\n '🤷': {\r\n en: ['shrug', 'uncertain', 'indifferent'],\r\n ru: ['пожимание плечами', 'неуверенный', 'безразличный']\r\n },\r\n\r\n // Professional\r\n '👮': {\r\n en: ['police', 'officer', 'law'],\r\n ru: ['полицейский', 'офицер', 'закон']\r\n },\r\n '🕵️': {\r\n en: ['detective', 'spy', 'investigate'],\r\n ru: ['детектив', 'шпион', 'расследование']\r\n },\r\n '💂': {\r\n en: ['guard', 'soldier', 'military'],\r\n ru: ['страж', 'солдат', 'военный']\r\n },\r\n '🥷': {\r\n en: ['ninja', 'stealth', 'assassin'],\r\n ru: ['ниндзя', 'скрытность', 'ассассин']\r\n },\r\n '👷': {\r\n en: ['construction', 'worker', 'helmet'],\r\n ru: ['строитель', 'рабочий', 'шлем']\r\n },\r\n '🤴': {\r\n en: ['prince', 'royalty', 'king'],\r\n ru: ['принц', 'королевская семья', 'король']\r\n },\r\n '👸': {\r\n en: ['princess', 'royalty', 'queen'],\r\n ru: ['принцесса', 'королевская семья', 'королева']\r\n },\r\n '👳': {\r\n en: ['turban', 'cultural', 'tradition'],\r\n ru: ['тюрбан', 'культура', 'традиция']\r\n },\r\n '👲': {\r\n en: ['man with cap', 'cultural', 'traditional'],\r\n ru: ['мужчина в кепке', 'культура', 'традиционный']\r\n },\r\n '🧕': {\r\n en: ['woman with headscarf', 'cultural', 'modest'],\r\n ru: ['женщина в платке', 'культурная', 'скромная']\r\n },\r\n '🤵': {\r\n en: ['tuxedo', 'groom', 'formal'],\r\n ru: ['смокинг', 'жених', 'официальный']\r\n },\r\n '👰': {\r\n en: ['bride', 'wedding', 'formal'],\r\n ru: ['невеста', 'свадьба', 'официальный']\r\n },\r\n '🤰': {\r\n en: ['pregnant', 'expecting', 'mother'],\r\n ru: ['беременная', 'ожидающая', 'мать']\r\n },\r\n '🤱': {\r\n en: ['nursing', 'mother', 'baby'],\r\n ru: ['кормящая', 'мать', 'ребенок']\r\n },\r\n '👼': {\r\n en: ['angel', 'cherub', 'divine'],\r\n ru: ['ангел', 'херувим', 'божественный']\r\n },\r\n '🎅': {\r\n en: ['santa', 'christmas', 'jolly'],\r\n ru: ['Санта', 'Рождество', 'радостный']\r\n },\r\n '🤶': {\r\n en: ['mrs claus', 'christmas', 'holiday'],\r\n ru: ['миссис Клаус', 'Рождество', 'праздник']\r\n },\r\n '🦸': {\r\n en: ['superhero', 'power', 'hero'],\r\n ru: ['супергерой', 'сила', 'герой']\r\n },\r\n '🦹': {\r\n en: ['villain', 'bad', 'criminal'],\r\n ru: ['злодей', 'плохой', 'преступник']\r\n },\r\n '🧙': {\r\n en: ['wizard', 'magic', 'sorcery'],\r\n ru: ['волшебник', 'магия', 'колдовство']\r\n },\r\n '🧚': {\r\n en: ['fairy', 'magic', 'mystical'],\r\n ru: ['фея', 'магия', 'мистический']\r\n },\r\n '🧛': {\r\n en: ['vampire', 'dracula', 'undead'],\r\n ru: ['вампир', 'Дракула', 'нежить']\r\n },\r\n '🧜': {\r\n en: ['mermaid', 'mythical', 'ocean'],\r\n ru: ['русалка', 'мифическая', 'океан']\r\n },\r\n\r\n //------------------------- Animals & Nature -------------------------\r\n // Mammals\r\n '🐵': {\r\n en: ['monkey', 'ape', 'funny', 'mammal'],\r\n ru: ['обезьяна', 'примат', 'смешной', 'млекопитающее']\r\n },\r\n '🐒': {\r\n en: ['monkey', 'primate', 'curious'],\r\n ru: ['обезьяна', 'примат', 'любопытный']\r\n },\r\n '🦍': {\r\n en: ['gorilla', 'ape', 'strong', 'wild'],\r\n ru: ['горилла', 'обезьяна', 'сильный', 'дикий']\r\n },\r\n '🦧': {\r\n en: ['orangutan', 'ape', 'wild', 'mammal'],\r\n ru: ['орангутан', 'обезьяна', 'дикий', 'млекопитающее']\r\n },\r\n '🐶': {\r\n en: ['dog', 'puppy', 'pet', 'mammal'],\r\n ru: ['собака', 'щенок', 'домашний питомец', 'млекопитающее']\r\n },\r\n '🐕': {\r\n en: ['dog', 'canine', 'pet'],\r\n ru: ['собака', 'псовой', 'домашний питомец']\r\n },\r\n '🦮': {\r\n en: ['guide dog', 'service', 'assistance'],\r\n ru: ['собака-поводырь', 'служебная', 'помощь']\r\n },\r\n '🐕🦺': {\r\n en: ['service dog', 'working', 'assistance'],\r\n ru: ['служебная собака', 'рабочая', 'помощь']\r\n },\r\n '🐩': {\r\n en: ['poodle', 'dog', 'pet', 'fancy'],\r\n ru: ['пудель', 'собака', 'домашний питомец', 'элегантный']\r\n },\r\n '🐺': {\r\n en: ['wolf', 'wild', 'howl'],\r\n ru: ['волк', 'дикий', 'воет']\r\n },\r\n '🦊': {\r\n en: ['fox', 'cunning', 'wild'],\r\n ru: ['лиса', 'хитрая', 'дикая']\r\n },\r\n '🦝': {\r\n en: ['raccoon', 'mischievous', 'wild'],\r\n ru: ['енот', 'озорной', 'дикий']\r\n },\r\n '🐱': {\r\n en: ['cat', 'pet', 'feline'],\r\n ru: ['кот', 'домашний питомец', 'кошачий']\r\n },\r\n '🐈': {\r\n en: ['cat', 'feline', 'pet'],\r\n ru: ['кот', 'кошачий', 'домашний питомец']\r\n },\r\n '🐈⬛': {\r\n en: ['black cat', 'mysterious', 'feline'],\r\n ru: ['чёрный кот', 'загадочный', 'кошачий']\r\n },\r\n '🦁': {\r\n en: ['lion', 'king', 'wild', 'courage'],\r\n ru: ['лев', 'король', 'дикий', 'отвага']\r\n },\r\n '🐯': {\r\n en: ['tiger', 'wild', 'stripes', 'fierce'],\r\n ru: ['тигр', 'дикий', 'полосатый', 'свирепый']\r\n },\r\n '🐅': {\r\n en: ['tiger', 'stripes', 'wild'],\r\n ru: ['тигр', 'полосатый', 'дикий']\r\n },\r\n '🐆': {\r\n en: ['leopard', 'spots', 'wild', 'fast'],\r\n ru: ['леопард', 'пятна', 'дикий', 'быстрый']\r\n },\r\n '🐴': {\r\n en: ['horse', 'ride', 'equine'],\r\n ru: ['лошадь', 'езда', 'конный']\r\n },\r\n '🐎': {\r\n en: ['horse', 'racing', 'equine'],\r\n ru: ['лошадь', 'гонки', 'конный']\r\n },\r\n '🦄': {\r\n en: ['unicorn', 'magical', 'fantasy'],\r\n ru: ['единорог', 'волшебный', 'фэнтези']\r\n },\r\n '🦓': {\r\n en: ['zebra', 'stripes', 'wild'],\r\n ru: ['зебра', 'полосатая', 'дикая']\r\n },\r\n '🦌': {\r\n en: ['deer', 'antlers', 'forest'],\r\n ru: ['олень', 'рога', 'лес']\r\n },\r\n '🦬': {\r\n en: ['bison', 'buffalo', 'wild'],\r\n ru: ['бизон', 'буйвол', 'дикий']\r\n },\r\n '🐮': {\r\n en: ['cow', 'farm', 'bovine'],\r\n ru: ['корова', 'ферма', 'коровий']\r\n },\r\n '🐂': {\r\n en: ['ox', 'bull', 'bovine'],\r\n ru: ['вол', 'бык', 'коровий']\r\n },\r\n '🐃': {\r\n en: ['water buffalo', 'bovine', 'farm'],\r\n ru: ['водный буйвол', 'коровий', 'ферма']\r\n },\r\n '🐄': {\r\n en: ['cow', 'bovine', 'farm'],\r\n ru: ['корова', 'коровий', 'ферма']\r\n },\r\n '🐷': {\r\n en: ['pig', 'farm', 'oink'],\r\n ru: ['свинья', 'ферма', 'хрюк']\r\n },\r\n '🐖': {\r\n en: ['pig', 'swine', 'farm'],\r\n ru: ['свинья', 'свинное животное', 'ферма']\r\n },\r\n '🐗': {\r\n en: ['boar', 'wild', 'pig'],\r\n ru: ['кабан', 'дикий', 'свинья']\r\n },\r\n '🐽': {\r\n en: ['pig nose', 'snout'],\r\n ru: ['свинной нос', 'хоботок']\r\n },\r\n '🐏': {\r\n en: ['ram', 'sheep', 'male'],\r\n ru: ['баран', 'овца', 'самец']\r\n },\r\n '🐑': {\r\n en: ['sheep', 'wool', 'farm'],\r\n ru: ['овца', 'шерсть', 'ферма']\r\n },\r\n '🐐': {\r\n en: ['goat', 'farm', 'bleat'],\r\n ru: ['коза', 'ферма', 'блеет']\r\n },\r\n '🐪': {\r\n en: ['camel', 'desert', 'hump'],\r\n ru: ['верблюд', 'пустыня', 'горб']\r\n },\r\n '🐫': {\r\n en: ['camel', 'two-hump', 'desert'],\r\n ru: ['двугорбый верблюд', 'двугорбый', 'пустыня']\r\n },\r\n '🦙': {\r\n en: ['llama', 'alpaca', 'cute'],\r\n ru: ['лама', 'альпака', 'милый']\r\n },\r\n '🦒': {\r\n en: ['giraffe', 'tall', 'spots'],\r\n ru: ['жираф', 'высокий', 'пятна']\r\n },\r\n '🐘': {\r\n en: ['elephant', 'trunk', 'large'],\r\n ru: ['слон', 'хобот', 'большой']\r\n },\r\n '🦏': {\r\n en: ['rhinoceros', 'horn', 'tough'],\r\n ru: ['носорог', 'рог', 'жесткий']\r\n },\r\n '🦛': {\r\n en: ['hippopotamus', 'water', 'large'],\r\n ru: ['бегемот', 'вода', 'большой']\r\n },\r\n\r\n // Birds\r\n '🦃': {\r\n en: ['turkey', 'bird', 'thanksgiving'],\r\n ru: ['индейка', 'птица', 'День благодарения']\r\n },\r\n '🐔': {\r\n en: ['chicken', 'rooster', 'hen'],\r\n ru: ['курица', 'петух', 'курица (самка)']\r\n },\r\n '🐓': {\r\n en: ['rooster', 'chicken', 'bird'],\r\n ru: ['петух', 'курица', 'птица']\r\n },\r\n '🐣': {\r\n en: ['hatching chick', 'baby', 'bird'],\r\n ru: ['вылупляющийся цыплёнок', 'малыш', 'птица']\r\n },\r\n '🐤': {\r\n en: ['chick', 'small', 'bird'],\r\n ru: ['цыплёнок', 'маленький', 'птица']\r\n },\r\n '🐥': {\r\n en: ['baby chicken', 'cute', 'bird'],\r\n ru: ['цыплёнок', 'милый', 'птица']\r\n },\r\n '🐦': {\r\n en: ['bird', 'tweet', 'wing'],\r\n ru: ['птица', 'чирик', 'крыло']\r\n },\r\n '🐧': {\r\n en: ['penguin', 'cold', 'bird'],\r\n ru: ['пингвин', 'холодный', 'птица']\r\n },\r\n '🕊️': {\r\n en: ['dove', 'peace', 'bird'],\r\n ru: ['голубь', 'мир', 'птица']\r\n },\r\n '🦅': {\r\n en: ['eagle', 'wild', 'bird'],\r\n ru: ['орёл', 'дикий', 'птица']\r\n },\r\n '🦆': {\r\n en: ['duck', 'water', 'bird'],\r\n ru: ['утка', 'вода', 'птица']\r\n },\r\n '🦢': {\r\n en: ['swan', 'graceful', 'bird'],\r\n ru: ['лебедь', 'грациозный', 'птица']\r\n },\r\n '🦉': {\r\n en: ['owl', 'wise', 'night', 'bird'],\r\n ru: ['сова', 'мудрая', 'ночная', 'птица']\r\n },\r\n '🦤': {\r\n en: ['dodo', 'extinct', 'bird'],\r\n ru: ['додо', 'вымершая', 'птица']\r\n },\r\n '🪶': {\r\n en: ['feather', 'light', 'bird'],\r\n ru: ['перо', 'лёгкое', 'птица']\r\n },\r\n '🦩': {\r\n en: ['flamingo', 'pink', 'bird'],\r\n ru: ['фламинго', 'розовый', 'птица']\r\n },\r\n '🦚': {\r\n en: ['peacock', 'colorful', 'bird'],\r\n ru: ['павлин', 'яркий', 'птица']\r\n },\r\n '🦜': {\r\n en: ['parrot', 'talkative', 'colorful'],\r\n ru: ['попугай', 'болтливый', 'яркий']\r\n },\r\n\r\n // Reptiles/Amphibians\r\n '🐸': {\r\n en: ['frog', 'amphibian', 'green'],\r\n ru: ['лягушка', 'амфибия', 'зелёная']\r\n },\r\n '🐊': {\r\n en: ['crocodile', 'reptile', 'danger'],\r\n ru: ['крокодил', 'рептилия', 'опасность']\r\n },\r\n '🐢': {\r\n en: ['turtle', 'slow', 'reptile'],\r\n ru: ['черепаха', 'медленная', 'рептилия']\r\n },\r\n '🦎': {\r\n en: ['lizard', 'reptile', 'scaly'],\r\n ru: ['ящерица', 'рептилия', 'чешуйчатая']\r\n },\r\n '🐍': {\r\n en: ['snake', 'reptile', 'slither'],\r\n ru: ['змея', 'рептилия', 'ползать']\r\n },\r\n '🐲': {\r\n en: ['dragon face', 'mythical', 'dragon'],\r\n ru: ['лицо дракона', 'мифический', 'дракон']\r\n },\r\n '🐉': {\r\n en: ['dragon', 'mythical', 'fire'],\r\n ru: ['дракон', 'мифический', 'огонь']\r\n },\r\n '🦕': {\r\n en: ['dinosaur', 'sauropod', 'prehistoric'],\r\n ru: ['динозавр', 'зауропод', 'доисторический']\r\n },\r\n '🦖': {\r\n en: ['dinosaur', 'T-Rex', 'prehistoric'],\r\n ru: ['динозавр', 'Ти-Рекс', 'доисторический']\r\n },\r\n\r\n // Marine\r\n '🐳': {\r\n en: ['whale', 'ocean', 'large'],\r\n ru: ['кит', 'океан', 'большой']\r\n },\r\n '🐋': {\r\n en: ['whale', 'ocean', 'big'],\r\n ru: ['кит', 'океан', 'огромный']\r\n },\r\n '🐬': {\r\n en: ['dolphin', 'ocean', 'friendly'],\r\n ru: ['дельфин', 'океан', 'дружелюбный']\r\n },\r\n '🦭': {\r\n en: ['seal', 'marine', 'cute'],\r\n ru: ['тюлень', 'морской', 'милый']\r\n },\r\n '🐟': {\r\n en: ['fish', 'ocean', 'swim'],\r\n ru: ['рыба', 'океан', 'плавать']\r\n },\r\n '🐠': {\r\n en: ['tropical fish', 'ocean', 'colorful'],\r\n ru: ['тропическая рыба', 'океан', 'яркая']\r\n },\r\n '🐡': {\r\n en: ['blowfish', 'puffer', 'ocean'],\r\n ru: ['рыба-иглобрюх', 'фугу', 'океан']\r\n },\r\n '🦈': {\r\n en: ['shark', 'ocean', 'dangerous'],\r\n ru: ['акула', 'океан', 'опасная']\r\n },\r\n '🐙': {\r\n en: ['octopus', 'marine', 'tentacles'],\r\n ru: ['осьминог', 'морской', 'щупальца']\r\n },\r\n '🐚': {\r\n en: ['shell', 'beach', 'ocean'],\r\n ru: ['ракушка', 'пляж', 'океан']\r\n },\r\n '🪸': {\r\n en: ['coral', 'reef', 'ocean'],\r\n ru: ['коралл', 'риф', 'океан']\r\n },\r\n\r\n // Insects\r\n '🐌': {\r\n en: ['snail', 'slow', 'mollusk'],\r\n ru: ['улитка', 'медленная', 'моллюск']\r\n },\r\n '🦋': {\r\n en: ['butterfly', 'insect', 'colorful'],\r\n ru: ['бабочка', 'насекомое', 'яркая']\r\n },\r\n '🐛': {\r\n en: ['caterpillar', 'insect', 'larva'],\r\n ru: ['гусеница', 'насекомое', 'личинка']\r\n },\r\n '🐜': {\r\n en: ['ant', 'small', 'insect'],\r\n ru: ['муравей', 'маленький', 'насекомое']\r\n },\r\n '🐝': {\r\n en: ['bee', 'insect', 'honey'],\r\n ru: ['пчела', 'насекомое', 'мёд']\r\n },\r\n '🪲': {\r\n en: ['beetle', 'insect', 'bug'],\r\n ru: ['жук', 'насекомое', 'баг']\r\n },\r\n '🐞': {\r\n en: ['ladybug', 'insect', 'lucky'],\r\n ru: ['божья коровка', 'насекомое', 'счастливая']\r\n },\r\n '🦗': {\r\n en: ['cricket', 'insect', 'chirp'],\r\n ru: ['сверчок', 'насекомое', 'щебет']\r\n },\r\n '🪳': {\r\n en: ['cockroach', 'insect', 'pest'],\r\n ru: ['таракан', 'насекомое', 'вредитель']\r\n },\r\n '🕷️': {\r\n en: ['spider', 'arachnid', 'insect'],\r\n ru: ['паук', 'паукообразное', 'насекомое']\r\n },\r\n '🕸️': {\r\n en: ['web', 'spider', 'trap'],\r\n ru: ['паутина', 'паук', 'ловушка']\r\n },\r\n '🦂': {\r\n en: ['scorpion', 'insect', 'venom'],\r\n ru: ['скорпион', 'насекомое', 'яд']\r\n },\r\n '🦟': {\r\n en: ['mosquito', 'insect', 'bite'],\r\n ru: ['комар', 'насекомое', 'укус']\r\n },\r\n '🪰': {\r\n en: ['fly', 'insect', 'buzz'],\r\n ru: ['муха', 'насекомое', 'жужжание']\r\n },\r\n '🪱': {\r\n en: ['worm', 'earth', 'invertebrate'],\r\n ru: ['червь', 'земля', 'беспозвоночное']\r\n },\r\n\r\n // Plants\r\n '🌸': {\r\n en: ['cherry blossom', 'flower', 'spring'],\r\n ru: ['сакура', 'цветок', 'весна']\r\n },\r\n '💮': {\r\n en: ['white flower', 'flower', 'symbol'],\r\n ru: ['белый цветок', 'цветок', 'символ']\r\n },\r\n '🏵️': {\r\n en: ['rosette', 'flower', 'decorative'],\r\n ru: ['розетка', 'цветок', 'декоративный']\r\n },\r\n '🌹': {\r\n en: ['rose', 'flower', 'love', 'romance'],\r\n ru: ['роза', 'цветок', 'любовь', 'романтика']\r\n },\r\n '🥀': {\r\n en: ['wilted flower', 'sad', 'decay'],\r\n ru: ['увядший цветок', 'грусть', 'разложение']\r\n },\r\n '🌺': {\r\n en: ['hibiscus', 'flower', 'tropical'],\r\n ru: ['гибискус', 'цветок', 'тропический']\r\n },\r\n '🌻': {\r\n en: ['sunflower', 'flower', 'summer'],\r\n ru: ['подсолнух', 'цветок', 'лето']\r\n },\r\n '🌼': {\r\n en: ['blossom', 'flower', 'spring'],\r\n ru: ['цветение', 'цветок', 'весна']\r\n },\r\n '🌷': {\r\n en: ['tulip', 'flower', 'spring'],\r\n ru: ['тюльпан', 'цветок', 'весна']\r\n },\r\n '🌱': {\r\n en: ['seedling', 'plant', 'growth'],\r\n ru: ['сеянец', 'растение', 'рост']\r\n },\r\n '🪴': {\r\n en: ['potted plant', 'indoor', 'green'],\r\n ru: ['растение в горшке', 'в помещении', 'зелёное']\r\n },\r\n '🌲': {\r\n en: ['evergreen', 'tree', 'forest'],\r\n ru: ['вечнозелёное', 'дерево', 'лес']\r\n },\r\n '🌳': {\r\n en: ['tree', 'nature', 'shade'],\r\n ru: ['дерево', 'природа', 'тень']\r\n },\r\n '🌴': {\r\n en: ['palm tree', 'tropical', 'beach'],\r\n ru: ['пальма', 'тропический', 'пляж']\r\n },\r\n '🌵': {\r\n en: ['cactus', 'desert', 'succulent'],\r\n ru: ['кактус', 'пустыня', 'суккулент']\r\n },\r\n '🌾': {\r\n en: ['sheaf', 'grain', 'farm'],\r\n ru: ['сноп', 'зерно', 'ферма']\r\n },\r\n '🌿': {\r\n en: ['herb', 'plant', 'leaf'],\r\n ru: ['трава', 'растение', 'лист']\r\n },\r\n '☘️': {\r\n en: ['shamrock', 'luck', 'clover'],\r\n ru: ['клевер', 'удача', 'трилистник']\r\n },\r\n '🍀': {\r\n en: ['four-leaf clover', 'luck', 'green'],\r\n ru: ['клевер с четырьмя листьями', 'удача', 'зелёный']\r\n },\r\n '🍁': {\r\n en: ['maple leaf', 'autumn', 'fall'],\r\n ru: ['кленовый лист', 'осень', 'осенний']\r\n },\r\n '🍂': {\r\n en: ['fallen leaf', 'autumn', 'nature'],\r\n ru: ['опавший лист', 'осень', 'природа']\r\n },\r\n '🍃': {\r\n en: ['leaf', 'wind', 'nature'],\r\n ru: ['лист', 'ветер', 'природа']\r\n },\r\n\r\n //------------------------- Food & Drink -------------------------\r\n // Fruits\r\n '🍎': {\r\n en: ['apple', 'red', 'fruit', 'healthy'],\r\n ru: ['яблоко', 'красное', 'фрукт', 'полезное']\r\n },\r\n '🍐': {\r\n en: ['pear', 'fruit', 'green', 'juicy'],\r\n ru: ['груша', 'фрукт', 'зелёная', 'сочная']\r\n },\r\n '🍊': {\r\n en: ['orange', 'fruit', 'citrus', 'vitamin C'],\r\n ru: ['апельсин', 'фрукт', 'цитрус', 'витамин C']\r\n },\r\n '🍋': {\r\n en: ['lemon', 'citrus', 'sour', 'yellow'],\r\n ru: ['лимон', 'цитрус', 'кислый', 'жёлтый']\r\n },\r\n '🍌': {\r\n en: ['banana', 'fruit', 'yellow', 'tropical'],\r\n ru: ['банан', 'фрукт', 'жёлтый', 'тропический']\r\n },\r\n '🍉': {\r\n en: ['watermelon', 'fruit', 'summer', 'refreshing'],\r\n ru: ['арбуз', 'фрукт', 'лето', 'освежающий']\r\n },\r\n '🍇': {\r\n en: ['grapes', 'fruit', 'purple', 'vine'],\r\n ru: ['виноград', 'фрукт', 'фиолетовый', 'виноградная лоза']\r\n },\r\n '🍓': {\r\n en: ['strawberry', 'fruit', 'red', 'sweet'],\r\n ru: ['клубника', 'фрукт', 'красная', 'сладкая']\r\n },\r\n '🫐': {\r\n en: ['blueberry', 'fruit', 'blue', 'healthy'],\r\n ru: ['черника', 'фрукт', 'синяя', 'полезная']\r\n },\r\n '🍈': {\r\n en: ['melon', 'fruit', 'sweet', 'green'],\r\n ru: ['дыня', 'фрукт', 'сладкая', 'зелёная']\r\n },\r\n '🍒': {\r\n en: ['cherry', 'fruit', 'red', 'sweet'],\r\n ru: ['вишня', 'фрукт', 'красная', 'сладкая']\r\n },\r\n '🍑': {\r\n en: ['peach', 'fruit', 'fuzzy', 'sweet'],\r\n ru: ['персик', 'фрукт', 'шероховатый', 'сладкий']\r\n },\r\n '🥭': {\r\n en: ['mango', 'fruit', 'tropical', 'yellow'],\r\n ru: ['манго', 'фрукт', 'тропический', 'жёлтый']\r\n },\r\n '🍍': {\r\n en: ['pineapple', 'fruit', 'tropical', 'spiky'],\r\n ru: ['ананас', 'фрукт', 'тропический', 'колючий']\r\n },\r\n '🥥': {\r\n en: ['coconut', 'tropical', 'fruit', 'white'],\r\n ru: ['кокос', 'тропический', 'фрукт', 'белый']\r\n },\r\n '🥝': {\r\n en: ['kiwi', 'fruit', 'green', 'tart'],\r\n ru: ['киви', 'фрукт', 'зелёный', 'кисловатый']\r\n },\r\n\r\n // Vegetables\r\n '🍅': {\r\n en: ['tomato', 'vegetable', 'red', 'juicy'],\r\n ru: ['помидор', 'овощ', 'красный', 'сочный']\r\n },\r\n '🍆': {\r\n en: ['eggplant', 'vegetable', 'purple', 'aubergine'],\r\n ru: ['баклажан', 'овощ', 'фиолетовый', 'баклажан']\r\n },\r\n '🥑': {\r\n en: ['avocado', 'vegetable', 'green', 'healthy'],\r\n ru: ['авокадо', 'овощ', 'зелёный', 'полезный']\r\n },\r\n '🥦': {\r\n en: ['broccoli', 'vegetable', 'green', 'healthy'],\r\n ru: ['брокколи', 'овощ', 'зелёный', 'полезный']\r\n },\r\n '🥬': {\r\n en: ['leafy greens', 'vegetable', 'lettuce', 'healthy'],\r\n ru: ['листья салата', 'овощ', 'салат', 'полезный']\r\n },\r\n '🥒': {\r\n en: ['cucumber', 'vegetable', 'green', 'fresh'],\r\n ru: ['огурец', 'овощ', 'зелёный', 'свежий']\r\n },\r\n '🌶️': {\r\n en: ['chili pepper', 'spicy', 'red', 'hot'],\r\n ru: ['чили', 'острый', 'красный', 'горячий']\r\n },\r\n '🫑': {\r\n en: ['bell pepper', 'vegetable', 'colorful', 'sweet'],\r\n ru: ['болгарский перец', 'овощ', 'разноцветный', 'сладкий']\r\n },\r\n '🥕': {\r\n en: ['carrot', 'vegetable', 'orange', 'crunchy'],\r\n ru: ['морковь', 'овощ', 'оранжевая', 'хрустящая']\r\n },\r\n '🧄': {\r\n en: ['garlic', 'vegetable', 'aromatic', 'flavorful'],\r\n ru: ['чеснок', 'овощ', 'ароматный', 'вкусный']\r\n },\r\n '🧅': {\r\n en: ['onion', 'vegetable', 'strong', 'flavor'],\r\n ru: ['лук', 'овощ', 'сильный', 'вкус']\r\n },\r\n '🥔': {\r\n en: ['potato', 'vegetable', 'starchy', 'brown'],\r\n ru: ['картофель', 'овощ', 'крахмалистый', 'коричневый']\r\n },\r\n '🍠': {\r\n en: ['sweet potato', 'vegetable', 'orange', 'starchy'],\r\n ru: ['батат', 'овощ', 'оранжевый', 'крахмалистый']\r\n },\r\n\r\n // Breads & Baked Goods\r\n '🥐': {\r\n en: ['croissant', 'bread', 'pastry', 'flaky'],\r\n ru: ['круассан', 'хлеб', 'выпечка', 'слоёный']\r\n },\r\n '🥯': {\r\n en: ['bagel', 'bread', 'round', 'chewy'],\r\n ru: ['бейгл', 'хлеб', 'круглый', 'жевательный']\r\n },\r\n '🍞': {\r\n en: ['bread', 'baked', 'loaf', 'toast'],\r\n ru: ['хлеб', 'выпеченный', 'буханка', 'тост']\r\n },\r\n '🥖': {\r\n en: ['baguette', 'bread', 'French', 'long'],\r\n ru: ['багет', 'хлеб', 'французский', 'длинный']\r\n },\r\n '🥨': {\r\n en: ['pretzel', 'snack', 'salted', 'twisted'],\r\n ru: ['претцель', 'закуска', 'солёный', 'скрученный']\r\n },\r\n\r\n // Prepared Foods\r\n '🧀': {\r\n en: ['cheese', 'dairy', 'yellow', 'savory'],\r\n ru: ['сыр', 'молочный продукт', 'жёлтый', 'пикантный']\r\n },\r\n '🥚': {\r\n en: ['egg', 'protein', 'breakfast'],\r\n ru: ['яйцо', 'белок', 'завтрак']\r\n },\r\n '🍳': {\r\n en: ['fried egg', 'breakfast', 'cooked'],\r\n ru: ['жареное яйцо', 'завтрак', 'приготовленное']\r\n },\r\n '🥓': {\r\n en: ['bacon', 'meat', 'crispy', 'breakfast'],\r\n ru: ['бекон', 'мясо', 'хрустящий', 'завтрак']\r\n },\r\n '🥩': {\r\n en: ['steak', 'meat', 'protein', 'beef'],\r\n ru: ['стейк', 'мясо', 'белок', 'говядина']\r\n },\r\n '🍗': {\r\n en: ['chicken leg', 'meat', 'drumstick', 'grilled'],\r\n ru: ['куриная ножка', 'мясо', 'барабанная палочка', 'гриль']\r\n },\r\n '🍖': {\r\n en: ['meat on bone', 'barbecue', 'protein'],\r\n ru: ['мясо на кости', 'барбекю', 'белок']\r\n },\r\n '🦴': {\r\n en: ['bone', 'meat', 'dog', 'skeleton'],\r\n ru: ['кость', 'мясо', 'собака', 'скелет']\r\n },\r\n '🌭': {\r\n en: ['hot dog', 'fast food', 'sausage'],\r\n ru: ['хот-дог', 'фастфуд', 'колбаска']\r\n },\r\n '🍔': {\r\n en: ['burger', 'fast food', 'beef', 'cheese'],\r\n ru: ['бургер', 'фастфуд', 'говядина', 'сыр']\r\n },\r\n '🍟': {\r\n en: ['french fries', 'fast food', 'crispy', 'potato'],\r\n ru: ['картофель фри', 'фастфуд', 'хрустящий', 'картофель']\r\n },\r\n '🍕': {\r\n en: ['pizza', 'cheese', 'fast food', 'Italian'],\r\n ru: ['пицца', 'сыр', 'фастфуд', 'итальянская']\r\n },\r\n '🫓': {\r\n en: ['flatbread', 'bread', 'soft'],\r\n ru: ['лепёшка', 'хлеб', 'мягкая']\r\n },\r\n '🥪': {\r\n en: ['sandwich', 'bread', 'meal'],\r\n ru: ['бутерброд', 'хлеб', 'приём пищи']\r\n },\r\n '🥙': {\r\n en: ['pita', 'bread', 'stuffed', 'Greek'],\r\n ru: ['пита', 'хлеб', 'фаршированная', 'греческая']\r\n },\r\n '🧆': {\r\n en: ['falafel', 'vegetarian', 'fried'],\r\n ru: ['фалафель', 'вегетарианский', 'жареный']\r\n },\r\n '🌮': {\r\n en: ['taco', 'Mexican', 'spicy'],\r\n ru: ['тако', 'мексиканская', 'острая']\r\n },\r\n '🌯': {\r\n en: ['burrito', 'Mexican', 'stuffed'],\r\n ru: ['буррито', 'мексиканская', 'фаршированная']\r\n },\r\n '🫔': {\r\n en: ['tamale', 'Mexican', 'corn'],\r\n ru: ['тамале', 'мексиканская', 'кукурузная']\r\n },\r\n '🥗': {\r\n en: ['salad', 'healthy', 'vegetable'],\r\n ru: ['салат', 'здоровый', 'овощной']\r\n },\r\n\r\n // Asian Foods\r\n '🥘': {\r\n en: ['paella', 'stew', 'seafood'],\r\n ru: ['паэлья', 'тушёное блюдо', 'морепродукты']\r\n },\r\n '🫕': {\r\n en: ['fondue', 'melted', 'cheese'],\r\n ru: ['фондю', 'расплавленный', 'сыр']\r\n },\r\n '🥫': {\r\n en: ['canned food', 'storage', 'preserved'],\r\n ru: ['консервы', 'хранение', 'сохранённое']\r\n },\r\n '🍝': {\r\n en: ['spaghetti', 'pasta', 'Italian'],\r\n ru: ['спагетти', 'паста', 'итальянская']\r\n },\r\n '🍜': {\r\n en: ['ramen', 'noodles', 'Asian'],\r\n ru: ['рамен', 'лапша', 'азиатская']\r\n },\r\n '🍲': {\r\n en: ['hotpot', 'stew', 'broth'],\r\n ru: ['хотпот', 'тушёное блюдо', 'бульон']\r\n },\r\n '🍛': {\r\n en: ['curry', 'spicy', 'rice'],\r\n ru: ['карри', 'острая', 'рис']\r\n },\r\n '🍣': {\r\n en: ['sushi', 'Japanese', 'fish'],\r\n ru: ['суши', 'японская', 'рыба']\r\n },\r\n '🍱': {\r\n en: ['bento box', 'Japanese', 'meal'],\r\n ru: ['бенто', 'японская', 'еда']\r\n },\r\n '🥟': {\r\n en: ['dumpling', 'Asian', 'stuffed'],\r\n ru: ['пельмени', 'азиатские', 'фаршированные']\r\n },\r\n '🦪': {\r\n en: ['oyster', 'seafood', 'shellfish'],\r\n ru: ['устрица', 'морепродукт', 'раковина']\r\n },\r\n '🍤': {\r\n en: ['shrimp tempura', 'fried', 'seafood'],\r\n ru: ['креветка темпура', 'жареная', 'морепродукт']\r\n },\r\n '🍙': {\r\n en: ['rice ball', 'Japanese', 'onigiri'],\r\n ru: ['рисовый шар', 'японская', 'онигири']\r\n },\r\n '🍚': {\r\n en: ['cooked rice', 'staple', 'Asian'],\r\n ru: ['варёный рис', 'основа', 'азиатская']\r\n },\r\n '🍘': {\r\n en: ['rice cracker', 'snack', 'Japanese'],\r\n ru: ['рисовый крекер', 'закуска', 'японская']\r\n },\r\n '🍥': {\r\n en: ['fish cake', 'Japanese', 'swirl'],\r\n ru: ['рыбный пирожок', 'японская', 'спираль']\r\n },\r\n '🥠': {\r\n en: ['fortune cookie', 'Chinese', 'paper'],\r\n ru: ['печенье с предсказанием', 'китайское', 'бумажное']\r\n },\r\n '🥮': {\r\n en: ['mooncake', 'Chinese', 'festival'],\r\n ru: ['лунное печенье', 'китайское', 'фестиваль']\r\n },\r\n\r\n // Sweets & Desserts\r\n '🍢': {\r\n en: ['skewered snack', 'street food'],\r\n ru: ['шампур', 'уличная еда']\r\n },\r\n '🍡': {\r\n en: ['dango', 'Japanese', 'mochi'],\r\n ru: ['данго', 'японское', 'мочи']\r\n },\r\n '🍧': {\r\n en: ['shaved ice', 'dessert', 'cold'],\r\n ru: ['измельчённый лёд', 'десерт', 'холодный']\r\n },\r\n '🍨': {\r\n en: ['ice cream', 'cold', 'sweet'],\r\n ru: ['мороженое', 'холодное', 'сладкое']\r\n },\r\n '🍦': {\r\n en: ['soft serve', 'dessert', 'cold'],\r\n ru: ['мягкое мороженое', 'десерт', 'холодное']\r\n },\r\n '🥧': {\r\n en: ['pie', 'dessert', 'baked'],\r\n ru: ['пирог', 'десерт', 'выпеченный']\r\n },\r\n '🧁': {\r\n en: ['cupcake', 'sweet', 'frosting'],\r\n ru: ['капкейк', 'сладкий', 'с глазурью']\r\n },\r\n '🍰': {\r\n en: ['cake', 'dessert', 'slice'],\r\n ru: ['торт', 'десерт', 'кусок']\r\n },\r\n '🎂': {\r\n en: ['birthday cake', 'celebration', 'sweet'],\r\n ru: ['торт ко дню рождения', 'празднование', 'сладкий']\r\n },\r\n '🍮': {\r\n en: ['flan', 'custard', 'dessert'],\r\n ru: ['флан', 'кастард', 'десерт']\r\n },\r\n '🍭': {\r\n en: ['lollipop', 'candy', 'sweet'],\r\n ru: ['леденец', 'конфета', 'сладкий']\r\n },\r\n '🍬': {\r\n en: ['candy', 'sweet', 'sugar'],\r\n ru: ['конфета', 'сладость', 'сахар']\r\n },\r\n '🍫': {\r\n en: ['chocolate', 'sweet', 'cocoa'],\r\n ru: ['шоколад', 'сладкий', 'какао']\r\n },\r\n '🍿': {\r\n en: ['popcorn', 'snack', 'buttery'],\r\n ru: ['попкорн', 'закуска', 'масляный']\r\n },\r\n '🍩': {\r\n en: ['doughnut', 'sweet', 'fried'],\r\n ru: ['пончик', 'сладкий', 'жареный']\r\n },\r\n '🍪': {\r\n en: ['cookie', 'sweet', 'baked'],\r\n ru: ['печенье', 'сладкое', 'выпеченное']\r\n },\r\n\r\n // Drinks\r\n '🫖': {\r\n en: ['teapot', 'tea', 'hot'],\r\n ru: ['чайник', 'чай', 'горячий']\r\n },\r\n '☕': {\r\n en: ['coffee', 'hot drink', 'caffeine'],\r\n ru: ['кофе', 'горячий напиток', 'кофеин']\r\n },\r\n '🍵': {\r\n en: ['green tea', 'hot', 'Japanese'],\r\n ru: ['зелёный чай', 'горячий', 'японский']\r\n },\r\n '🧃': {\r\n en: ['juice', 'drink', 'fruit'],\r\n ru: ['сок', 'напиток', 'фруктовый']\r\n },\r\n '🥤': {\r\n en: ['soft drink', 'straw', 'fast food'],\r\n ru: ['безалкогольный напиток', 'с трубочкой', 'фастфуд']\r\n },\r\n '🧋': {\r\n en: ['bubble tea', 'milk tea', 'boba'],\r\n ru: ['бабл-ти', 'молочный чай', 'боба']\r\n },\r\n '🍶': {\r\n en: ['sake', 'Japanese', 'rice wine'],\r\n ru: ['саке', 'японский', 'рисовое вино']\r\n },\r\n '🍺': {\r\n en: ['beer', 'drink', 'alcohol'],\r\n ru: ['пиво', 'напиток', 'алкоголь']\r\n },\r\n '🍻': {\r\n en: ['cheers', 'beer', 'drinking'],\r\n ru: ['ура', 'пиво', 'выпивка']\r\n },\r\n '🥂': {\r\n en: ['champagne', 'celebration', 'toast'],\r\n ru: ['шампанское', 'празднование', 'тост']\r\n },\r\n '🍷': {\r\n en: ['wine', 'drink', 'red'],\r\n ru: ['вино', 'напиток', 'красное']\r\n },\r\n '🥃': {\r\n en: ['whiskey', 'liquor', 'alcohol'],\r\n ru: ['виски', 'ликёр', 'алкоголь']\r\n },\r\n '🍸': {\r\n en: ['cocktail', 'martini', 'drink'],\r\n ru: ['коктейль', 'мартини', 'напиток']\r\n },\r\n '🍹': {\r\n en: ['tropical drink', 'cocktail', 'summer'],\r\n ru: ['тропический напиток', 'коктейль', 'лето']\r\n },\r\n '🧉': {\r\n en: ['mate', 'South American', 'tea'],\r\n ru: ['мате', 'южноамериканский', 'чай']\r\n },\r\n '🍾': {\r\n en: ['champagne bottle', 'celebration', 'party'],\r\n ru: ['бутылка шампанского', 'празднование', 'вечеринка']\r\n },\r\n\r\n //------------------------- Activities & Objects -------------------------\r\n // Sports\r\n '⚽': {\r\n en: ['soccer', 'football', 'sports', 'ball'],\r\n ru: ['футбол', 'футбол', 'спорт', 'мяч']\r\n },\r\n '🏀': {\r\n en: ['basketball', 'sports', 'hoop', 'dunk'],\r\n ru: ['баскетбол', 'спорт', 'кольцо', 'данк']\r\n },\r\n '🏈': {\r\n en: ['American football', 'sports', 'rugby'],\r\n ru: ['американский футбол', 'спорт', 'регби']\r\n },\r\n '⚾': {\r\n en: ['baseball', 'sports', 'bat'],\r\n ru: ['бейсбол', 'спорт', 'бита']\r\n },\r\n '🥎': {\r\n en: ['softball', 'sports', 'ball'],\r\n ru: ['софтбол', 'спорт', 'мяч']\r\n },\r\n '🎾': {\r\n en: ['tennis', 'sports', 'racket'],\r\n ru: ['теннис', 'спорт', 'ракетка']\r\n },\r\n '🏐': {\r\n en: ['volleyball', 'sports', 'beach'],\r\n ru: ['волейбол', 'спорт', 'пляж']\r\n },\r\n '🏉': {\r\n en: ['rugby', 'sports', 'oval ball'],\r\n ru: ['регби', 'спорт', 'овальный мяч']\r\n },\r\n '🥏': {\r\n en: ['frisbee', 'throw', 'flying disc'],\r\n ru: ['фрисби', 'бросок', 'летающий диск']\r\n },\r\n '🎱': {\r\n en: ['billiards', '8 ball', 'pool'],\r\n ru: ['бильярд', 'восьмёрка', 'пул']\r\n },\r\n '🪀': {\r\n en: ['yo-yo', 'toy', 'string'],\r\n ru: ['йо-йо', 'игрушка', 'верёвка']\r\n },\r\n '🏓': {\r\n en: ['ping pong', 'table tennis', 'sports'],\r\n ru: ['пинг-понг', 'настольный теннис', 'спорт']\r\n },\r\n '🏸': {\r\n en: ['badminton', 'racket', 'sports'],\r\n ru: ['бадминтон', 'ракетка', 'спорт']\r\n },\r\n '🏒': {\r\n en: ['hockey', 'ice hockey', 'sports'],\r\n ru: ['хоккей', 'хоккей на льду', 'спорт']\r\n },\r\n '🏑': {\r\n en: ['field hockey', 'sports', 'stick'],\r\n ru: ['хоккей на траве', 'спорт', 'клюшка']\r\n },\r\n '🥍': {\r\n en: ['lacrosse', 'sports', 'net'],\r\n ru: ['лакросс', 'спорт', 'сеть']\r\n },\r\n '🏏': {\r\n en: ['cricket', 'bat', 'sports'],\r\n ru: ['крикет', 'бита', 'спорт']\r\n },\r\n '⛳': {\r\n en: ['golf', 'sports', 'hole in one'],\r\n ru: ['гольф', 'спорт', 'луночка']\r\n },\r\n '🪁': {\r\n en: ['kite', 'flying', 'wind'],\r\n ru: ['змей', 'летать', 'ветер']\r\n },\r\n '🎣': {\r\n en: ['fishing', 'hook', 'water'],\r\n ru: ['рыбалка', 'крючок', 'вода']\r\n },\r\n '🤿': {\r\n en: ['diving', 'underwater', 'snorkel'],\r\n ru: ['дайвинг', 'под водой', 'снорклинг']\r\n },\r\n '🎽': {\r\n en: ['running', 'jersey', 'athlete'],\r\n ru: ['бег', 'майка', 'спортсмен']\r\n },\r\n '🛹': {\r\n en: ['skateboard', 'sports', 'extreme'],\r\n ru: ['скейтборд', 'спорт', 'экстрим']\r\n },\r\n '🛼': {\r\n en: ['roller skate', 'sports', 'wheels'],\r\n ru: ['роликовые коньки', 'спорт', 'колёса']\r\n },\r\n '🛷': {\r\n en: ['sled', 'winter', 'snow'],\r\n ru: ['санки', 'зима', 'снег']\r\n },\r\n '⛸️': {\r\n en: ['ice skate', 'winter', 'sports'],\r\n ru: ['коньки', 'зима', 'спорт']\r\n },\r\n '🥌': {\r\n en: ['curling', 'winter', 'stone'],\r\n ru: ['керлинг', 'зима', 'камень']\r\n },\r\n '⛷️': {\r\n en: ['skiing', 'winter', 'snow'],\r\n ru: ['лыжи', 'зима', 'снег']\r\n },\r\n '🏂': {\r\n en: ['snowboarding', 'sports', 'snow'],\r\n ru: ['сноуборд', 'спорт', 'снег']\r\n },\r\n '🪂': {\r\n en: ['parachute', 'skydiving', 'air'],\r\n ru: ['парашют', 'скайдайвинг', 'воздух']\r\n },\r\n '🏋️': {\r\n en: ['weightlifting', 'gym', 'strong'],\r\n ru: ['тяжёлая атлетика', 'спортзал', 'сильный']\r\n },\r\n '🤼': {\r\n en: ['wrestling', 'fight', 'grapple'],\r\n ru: ['борьба', 'драка', 'схватка']\r\n },\r\n '🤸': {\r\n en: ['gymnastics', 'acrobatics', 'flip'],\r\n ru: ['гимнастика', 'акробатика', 'сальто']\r\n },\r\n '⛹️': {\r\n en: ['basketball', 'dribbling', 'sports'],\r\n ru: ['баскетбол', 'дриблинг', 'спорт']\r\n },\r\n '🤾': {\r\n en: ['handball', 'sports', 'throw'],\r\n ru: ['гандбол', 'спорт', 'бросок']\r\n },\r\n '🏌️': {\r\n en: ['golf', 'swing', 'sports'],\r\n ru: ['гольф', 'мах', 'спорт']\r\n },\r\n '🏇': {\r\n en: ['horse racing', 'sports', 'jockey'],\r\n ru: ['конные скачки', 'спорт', 'жокей']\r\n },\r\n '🧘': {\r\n en: ['meditation', 'yoga', 'zen'],\r\n ru: ['медитация', 'йога', 'дзен']\r\n },\r\n '🏄': {\r\n en: ['surfing', 'wave', 'water'],\r\n ru: ['серфинг', 'волна', 'вода']\r\n },\r\n '🏊': {\r\n en: ['swimming', 'water', 'pool'],\r\n ru: ['плавание', 'вода', 'бассейн']\r\n },\r\n\r\n // Activities (cultural/entertainment)\r\n '🤽': {\r\n en: ['water polo', 'sports', 'swimming'],\r\n ru: ['водное поло', 'спорт', 'плавание']\r\n },\r\n '🚣': {\r\n en: ['rowing', 'boat', 'water'],\r\n ru: ['гребля', 'лодка', 'вода']\r\n },\r\n '🧗': {\r\n en: ['rock climbing', 'sports', 'mountain'],\r\n ru: ['скалолазание', 'спорт', 'гора']\r\n },\r\n '🚴': {\r\n en: ['cycling', 'bike', 'sports'],\r\n ru: ['велоспорт', 'велосипед', 'спорт']\r\n },\r\n '🚵': {\r\n en: ['mountain biking', 'sports', 'outdoor'],\r\n ru: ['маунтинбайк', 'спорт', 'на свежем воздухе']\r\n },\r\n '🎪': {\r\n en: ['circus', 'tent', 'performance'],\r\n ru: ['цирк', 'палатка', 'выступление']\r\n },\r\n '🎭': {\r\n en: ['theater', 'drama', 'acting'],\r\n ru: ['театр', 'драма', 'актерство']\r\n },\r\n '🎨': {\r\n en: ['painting', 'art', 'colors'],\r\n ru: ['живопись', 'искусство', 'цвета']\r\n },\r\n '🎬': {\r\n en: ['film', 'clapperboard', 'movie'],\r\n ru: ['фильм', 'хлопушка', 'кино']\r\n },\r\n '🎤': {\r\n en: ['microphone', 'singing', 'music'],\r\n ru: ['микрофон', 'пение', 'музыка']\r\n },\r\n '🎧': {\r\n en: ['headphones', 'music', 'listening'],\r\n ru: ['наушники', 'музыка', 'прослушивание']\r\n },\r\n '🎼': {\r\n en: ['music', 'sheet', 'notes'],\r\n ru: ['музыка', 'ноты', 'сценарий']\r\n },\r\n '🎹': {\r\n en: ['piano', 'keyboard', 'music'],\r\n ru: ['пианино', 'клавиатура', 'музыка']\r\n },\r\n '🥁': {\r\n en: ['drums', 'music', 'percussion'],\r\n ru: ['барабаны', 'музыка', 'ударные']\r\n },\r\n '🎷': {\r\n en: ['saxophone', 'jazz', 'music'],\r\n ru: ['саксофон', 'джаз', 'музыка']\r\n },\r\n '🎺': {\r\n en: ['trumpet', 'brass', 'music'],\r\n ru: ['труба', 'латунь', 'музыка']\r\n },\r\n '🎸': {\r\n en: ['guitar', 'music', 'rock'],\r\n ru: ['гитара', 'музыка', 'рок']\r\n },\r\n '🎻': {\r\n en: ['violin', 'music', 'strings'],\r\n ru: ['скрипка', 'музыка', 'струны']\r\n },\r\n '🎲': {\r\n en: ['dice', 'game', 'board game'],\r\n ru: ['кубики', 'игра', 'настольная игра']\r\n },\r\n '🎯': {\r\n en: ['dart', 'target', 'game'],\r\n ru: ['дротик', 'цель', 'игра']\r\n },\r\n '🎳': {\r\n en: ['bowling', 'sports', 'pins'],\r\n ru: ['боулинг', 'спорт', 'кегли']\r\n },\r\n '🎮': {\r\n en: ['video game', 'console', 'gaming'],\r\n ru: ['видеоигра', 'консоль', 'игры']\r\n },\r\n '🎰': {\r\n en: ['slot machine', 'casino', 'gambling'],\r\n ru: ['игровой автомат', 'казино', 'азартные игры']\r\n },\r\n '🧩': {\r\n en: ['puzzle', 'pieces', 'brain teaser'],\r\n ru: ['головоломка', 'кусочки', 'разминка для мозга']\r\n },\r\n '🎫': {\r\n en: ['ticket', 'event', 'entry'],\r\n ru: ['билет', 'событие', 'вход']\r\n },\r\n '🎟️': {\r\n en: ['admission', 'ticket', 'event'],\r\n ru: ['входной билет', 'билет', 'событие']\r\n },\r\n\r\n //------------------------- Travel & Places -------------------------\r\n // Land Transport\r\n '🚗': {\r\n en: ['car', 'automobile', 'vehicle', 'transport'],\r\n ru: ['автомобиль', 'машина', 'транспортное средство', 'транспорт']\r\n },\r\n '🚕': {\r\n en: ['taxi', 'cab', 'transport', 'vehicle'],\r\n ru: ['такси', 'маршрутка', 'транспорт', 'транспортное средство']\r\n },\r\n '🚙': {\r\n en: ['suv', 'vehicle', 'transport'],\r\n ru: ['внедорожник', 'транспортное средство', 'транспорт']\r\n },\r\n '🚌': {\r\n en: ['bus', 'public transport', 'commute'],\r\n ru: ['автобус', 'общественный транспорт', 'коммьют']\r\n },\r\n '🚎': {\r\n en: ['trolleybus', 'public transport', 'electric'],\r\n ru: ['троллейбус', 'общественный транспорт', 'электрический']\r\n },\r\n '🏎️': {\r\n en: ['race car', 'sports car', 'fast', 'speed'],\r\n ru: ['гоночный автомобиль', 'спортивный автомобиль', 'быстрый', 'скорость']\r\n },\r\n '🚓': {\r\n en: ['police car', 'law enforcement', 'vehicle'],\r\n ru: ['полицейская машина', 'правоохранительные органы', 'транспортное средство']\r\n },\r\n '🚑': {\r\n en: ['ambulance', 'emergency', 'hospital'],\r\n ru: ['скорая помощь', 'чрезвычайная ситуация', 'больница']\r\n },\r\n '🚒': {\r\n en: ['fire truck', 'firefighter', 'emergency'],\r\n ru: ['пожарная машина', 'пожарный', 'чрезвычайная ситуация']\r\n },\r\n '🚐': {\r\n en: ['van', 'minibus', 'transport'],\r\n ru: ['фургон', 'маршрутка', 'транспорт']\r\n },\r\n '🛻': {\r\n en: ['pickup truck', 'off-road', 'vehicle'],\r\n ru: ['пикап', 'для бездорожья', 'транспортное средство']\r\n },\r\n '🚚': {\r\n en: ['delivery truck', 'freight', 'cargo'],\r\n ru: ['грузовик', 'доставка', 'груз']\r\n },\r\n '🚛': {\r\n en: ['truck', 'semi-trailer', 'transport'],\r\n ru: ['грузовик', 'самосвал', 'транспорт']\r\n },\r\n '🚜': {\r\n en: ['tractor', 'farming', 'agriculture'],\r\n ru: ['трактор', 'сельское хозяйство', 'агрономия']\r\n },\r\n '🛵': {\r\n en: ['scooter', 'motorbike', 'moped'],\r\n ru: ['скутер', 'мотоцикл', 'мопед']\r\n },\r\n '🏍️': {\r\n en: ['motorcycle', 'bike', 'racing'],\r\n ru: ['мотоцикл', 'байк', 'гоночный']\r\n },\r\n '🛺': {\r\n en: ['rickshaw', 'auto rickshaw', 'transport'],\r\n ru: ['рикша', 'авто-рикша', 'транспорт']\r\n },\r\n '🚲': {\r\n en: ['bicycle', 'bike', 'pedal', 'cycling'],\r\n ru: ['велосипед', 'байк', 'педаль', 'велоспорт']\r\n },\r\n '🛴': {\r\n en: ['kick scooter', 'scooter', 'ride'],\r\n ru: ['самокат', 'скутер', 'поездка']\r\n },\r\n\r\n // Air Transport\r\n '✈️': {\r\n en: ['airplane', 'flight', 'travel', 'sky'],\r\n ru: ['самолёт', 'рейс', 'путешествие', 'небо']\r\n },\r\n '🛩️': {\r\n en: ['small airplane', 'aviation', 'sky'],\r\n ru: ['малый самолёт', 'авиация', 'небо']\r\n },\r\n '🛫': {\r\n en: ['departure', 'takeoff', 'airport'],\r\n ru: ['вылет', 'взлёт', 'аэропорт']\r\n },\r\n '🛬': {\r\n en: ['landing', 'arrival', 'airport'],\r\n ru: ['посадка', 'прибытие', 'аэропорт']\r\n },\r\n '🚁': {\r\n en: ['helicopter', 'aviation', 'air'],\r\n ru: ['вертолёт', 'авиация', 'воздух']\r\n },\r\n '🚀': {\r\n en: ['rocket', 'space', 'launch', 'NASA'],\r\n ru: ['ракета', 'космос', 'запуск', 'НАСА']\r\n },\r\n '🛸': {\r\n en: ['UFO', 'alien', 'spaceship', 'extraterrestrial'],\r\n ru: ['НЛО', 'инопланетянин', 'космический корабль', 'внеземной']\r\n },\r\n\r\n // Water Transport\r\n '🛶': {\r\n en: ['canoe', 'boat', 'paddle', 'water'],\r\n ru: ['каноэ', 'лодка', 'весло', 'вода']\r\n },\r\n '⛵': {\r\n en: ['sailboat', 'yacht', 'sea'],\r\n ru: ['парусник', 'яхта', 'море']\r\n },\r\n '🚤': {\r\n en: ['motorboat', 'speedboat', 'ocean'],\r\n ru: ['моторная лодка', 'скоростной катер', 'океан']\r\n },\r\n '🛥️': {\r\n en: ['yacht', 'luxury', 'boat'],\r\n ru: ['яхта', 'роскошь', 'лодка']\r\n },\r\n '🛳️': {\r\n en: ['cruise ship', 'ocean', 'voyage'],\r\n ru: ['круизный лайнер', 'океан', 'путешествие']\r\n },\r\n '⛴️': {\r\n en: ['ferry', 'boat', 'transport'],\r\n ru: ['паром', 'лодка', 'транспорт']\r\n },\r\n '🚢': {\r\n en: ['ship', 'ocean', 'voyage'],\r\n ru: ['корабль', 'океан', 'путешествие']\r\n },\r\n\r\n // Places\r\n '🏰': {\r\n en: ['castle', 'fortress', 'history'],\r\n ru: ['замок', 'крепость', 'история']\r\n },\r\n '🏯': {\r\n en: ['Japanese castle', 'Asia', 'samurai'],\r\n ru: ['японский замок', 'Азия', 'самурай']\r\n },\r\n '🏟️': {\r\n en: ['stadium', 'sports', 'arena'],\r\n ru: ['стадион', 'спорт', 'арена']\r\n },\r\n '🏖️': {\r\n en: ['beach', 'sun', 'vacation'],\r\n ru: ['пляж', 'солнце', 'отпуск']\r\n },\r\n '🏝️': {\r\n en: ['island', 'tropical', 'vacation'],\r\n ru: ['остров', 'тропический', 'отпуск']\r\n },\r\n '🏜️': {\r\n en: ['desert', 'sand', 'dry'],\r\n ru: ['пустыня', 'песок', 'сухой']\r\n },\r\n '🌋': {\r\n en: ['volcano', 'eruption', 'lava'],\r\n ru: ['вулкан', 'извержение', 'лава']\r\n },\r\n '⛰️': {\r\n en: ['mountain', 'hiking', 'nature'],\r\n ru: ['гора', 'поход', 'природа']\r\n },\r\n '🏔️': {\r\n en: ['snowy mountain', 'climbing', 'cold'],\r\n ru: ['снежная гора', 'скалолазание', 'холод']\r\n },\r\n '🗻': {\r\n en: ['Mount Fuji', 'Japan', 'scenic'],\r\n ru: ['гора Фудзи', 'Япония', 'живописный']\r\n },\r\n '🏕️': {\r\n en: ['camping', 'tent', 'outdoors'],\r\n ru: ['кемпинг', 'палатка', 'на природе']\r\n },\r\n '🏭': {\r\n en: ['factory', 'industry', 'building'],\r\n ru: ['фабрика', 'индустрия', 'здание']\r\n },\r\n '🏢': {\r\n en: ['office building', 'corporate', 'city'],\r\n ru: ['офисное здание', 'корпоративный', 'город']\r\n },\r\n '🏬': {\r\n en: ['shopping mall', 'retail', 'stores'],\r\n ru: ['торговый центр', 'розничная торговля', 'магазины']\r\n },\r\n '🏣': {\r\n en: ['post office', 'mail', 'building'],\r\n ru: ['почтовое отделение', 'почта', 'здание']\r\n },\r\n '🏤': {\r\n en: ['post office', 'mail', 'postal service'],\r\n ru: ['почтовое отделение', 'почта', 'почтовая служба']\r\n },\r\n '🏥': {\r\n en: ['hospital', 'healthcare', 'emergency'],\r\n ru: ['больница', 'здравоохранение', 'чрезвычайная ситуация']\r\n },\r\n '🏦': {\r\n en: ['bank', 'money', 'finance'],\r\n ru: ['банк', 'деньги', 'финансы']\r\n },\r\n '🏨': {\r\n en: ['hotel', 'accommodation', 'stay'],\r\n ru: ['отель', 'размещение', 'проживание']\r\n },\r\n '🏪': {\r\n en: ['convenience store', 'shopping', '24/7'],\r\n ru: ['магазин', 'шоппинг', 'круглосуточно']\r\n },\r\n '🏫': {\r\n en: ['school', 'education', 'learning'],\r\n ru: ['школа', 'образование', 'обучение']\r\n },\r\n '🏩': {\r\n en: ['love hotel', 'romantic', 'Japan'],\r\n ru: ['любовный отель', 'романтический', 'Япония']\r\n },\r\n '💒': {\r\n en: ['wedding', 'church', 'marriage'],\r\n ru: ['свадьба', 'церковь', 'брак']\r\n },\r\n '⛪': {\r\n en: ['church', 'Christianity', 'religion'],\r\n ru: ['церковь', 'христианство', 'религия']\r\n },\r\n '🕌': {\r\n en: ['mosque', 'Islam', 'prayer'],\r\n ru: ['мечеть', 'ислам', 'молитва']\r\n },\r\n '🕍': {\r\n en: ['synagogue', 'Judaism', 'religion'],\r\n ru: ['синагога', 'иудаизм', 'религия']\r\n },\r\n '🛕': {\r\n en: ['hindu temple', 'spiritual', 'India'],\r\n ru: ['индуистский храм', 'духовный', 'Индия']\r\n },\r\n '⛩️': {\r\n en: ['shrine', 'torii', 'Japan'],\r\n ru: ['святилище', 'тории', 'Япония']\r\n },\r\n '🏛️': {\r\n en: ['government building', 'politics', 'history'],\r\n ru: ['правительственное здание', 'политика', 'история']\r\n },\r\n\r\n //------------------------- Objects & Symbols -------------------------\r\n // Tools & Technology\r\n '📱': {\r\n en: ['smartphone', 'mobile', 'phone'],\r\n ru: ['смартфон', 'мобильный', 'телефон']\r\n },\r\n '💻': {\r\n en: ['laptop', 'computer', 'technology'],\r\n ru: ['ноутбук', 'компьютер', 'технология']\r\n },\r\n '⌨️': {\r\n en: ['keyboard', 'typing', 'computer'],\r\n ru: ['клавиатура', 'набор текста', 'компьютер']\r\n },\r\n '🖥️': {\r\n en: ['desktop', 'monitor', 'screen'],\r\n ru: ['настольный компьютер', 'монитор', 'экран']\r\n },\r\n '🖨️': {\r\n en: ['printer', 'print', 'office'],\r\n ru: ['принтер', 'печать', 'офис']\r\n },\r\n '🖱️': {\r\n en: ['mouse', 'computer', 'click'],\r\n ru: ['мышь', 'компьютер', 'клик']\r\n },\r\n '🖲️': {\r\n en: ['trackball', 'navigation', 'input'],\r\n ru: ['трекбол', 'навигация', 'ввод']\r\n },\r\n '🕹️': {\r\n en: ['joystick', 'gaming', 'console'],\r\n ru: ['джойстик', 'игры', 'консоль']\r\n },\r\n '🗜️': {\r\n en: ['clamp', 'tool', 'hardware'],\r\n ru: ['зажим', 'инструмент', 'аппаратное обеспечение']\r\n },\r\n '💽': {\r\n en: ['mini disc', 'storage', 'media'],\r\n ru: ['мини-диск', 'хранение', 'медиа']\r\n },\r\n '💾': {\r\n en: ['floppy disk', 'save', 'data'],\r\n ru: ['флоппи-диск', 'сохранить', 'данные']\r\n },\r\n '💿': {\r\n en: ['CD', 'compact disc', 'music'],\r\n ru: ['CD', 'компакт-диск', 'музыка']\r\n },\r\n '📀': {\r\n en: ['DVD', 'video', 'disc'],\r\n ru: ['DVD', 'видео', 'диск']\r\n },\r\n '📼': {\r\n en: ['VHS', 'video cassette', 'retro'],\r\n ru: ['VHS', 'видеокассета', 'ретро']\r\n },\r\n '📷': {\r\n en: ['camera', 'photo', 'photography'],\r\n ru: ['камера', 'фото', 'фотография']\r\n },\r\n '📸': {\r\n en: ['camera flash', 'photography', 'snapshot'],\r\n ru: ['вспышка камеры', 'фотография', 'снимок']\r\n },\r\n '📹': {\r\n en: ['video camera', 'recording', 'film'],\r\n ru: ['видеокамера', 'запись', 'фильм']\r\n },\r\n '🎥': {\r\n en: ['movie camera', 'film', 'cinema'],\r\n ru: ['кино-камера', 'фильм', 'кино']\r\n },\r\n\r\n // Office & Communication\r\n '📞': {\r\n en: ['telephone', 'call', 'communication'],\r\n ru: ['телефон', 'звонок', 'коммуникация']\r\n },\r\n '☎️': {\r\n en: ['phone', 'landline', 'call'],\r\n ru: ['телефон', 'стационарный', 'звонок']\r\n },\r\n '📟': {\r\n en: ['pager', 'message', 'communication'],\r\n ru: ['пейджер', 'сообщение', 'коммуникация']\r\n },\r\n '📠': {\r\n en: ['fax machine', 'office', 'document'],\r\n ru: ['факс', 'офис', 'документ']\r\n },\r\n '📺': {\r\n en: ['TV', 'television', 'screen'],\r\n ru: ['телевизор', 'теле', 'экран']\r\n },\r\n '📻': {\r\n en: ['radio', 'broadcast', 'audio'],\r\n ru: ['радио', 'трансляция', 'аудио']\r\n },\r\n '🎙️': {\r\n en: ['microphone', 'recording', 'podcast'],\r\n ru: ['микрофон', 'запись', 'подкаст']\r\n },\r\n '🎚️': {\r\n en: ['control knob', 'audio', 'volume'],\r\n ru: ['ручка управления', 'аудио', 'громкость']\r\n },\r\n '🎛️': {\r\n en: ['sliders', 'equalizer', 'settings'],\r\n ru: ['ползунки', 'эквалайзер', 'настройки']\r\n },\r\n '📡': {\r\n en: ['satellite dish', 'broadcast', 'signal'],\r\n ru: ['спутниковая антенна', 'трансляция', 'сигнал']\r\n },\r\n '🔋': {\r\n en: ['battery', 'power', 'energy'],\r\n ru: ['батарея', 'мощность', 'энергия']\r\n },\r\n '🔌': {\r\n en: ['plug', 'electricity', 'charging'],\r\n ru: ['штекер', 'электричество', 'зарядка']\r\n },\r\n '💡': {\r\n en: ['light bulb', 'idea', 'illumination'],\r\n ru: ['лампочка', 'идея', 'освещение']\r\n },\r\n '🔦': {\r\n en: ['flashlight', 'torch', 'light'],\r\n ru: ['фонарик', 'фонарь', 'свет']\r\n },\r\n '🕯️': {\r\n en: ['candle', 'light', 'fire'],\r\n ru: ['свеча', 'свет', 'огонь']\r\n },\r\n\r\n // Household & Money\r\n '🧯': {\r\n en: ['fire extinguisher', 'safety', 'fire'],\r\n ru: ['огнетушитель', 'безопасность', 'огонь']\r\n },\r\n '🛢️': {\r\n en: ['barrel', 'oil', 'fuel'],\r\n ru: ['бочка', 'масло', 'топливо']\r\n },\r\n '💸': {\r\n en: ['money with wings', 'cash', 'spending'],\r\n ru: ['деньги с крыльями', 'наличные', 'расходы']\r\n },\r\n '💵': {\r\n en: ['dollar bills', 'money', 'USD'],\r\n ru: ['долларовые банкноты', 'деньги', 'USD']\r\n },\r\n '💴': {\r\n en: ['yen', 'money', 'Japan'],\r\n ru: ['иена', 'деньги', 'Япония']\r\n },\r\n '💶': {\r\n en: ['euro', 'money', 'currency'],\r\n ru: ['евро', 'деньги', 'валюта']\r\n },\r\n '💷': {\r\n en: ['pound', 'money', 'UK'],\r\n ru: ['фунт', 'деньги', 'Великобритания']\r\n },\r\n '🪙': {\r\n en: ['coin', 'currency', 'money'],\r\n ru: ['монета', 'валюта', 'деньги']\r\n },\r\n '💰': {\r\n en: ['money bag', 'wealth', 'rich'],\r\n ru: ['мешок с деньгами', 'богатство', 'богатый']\r\n },\r\n '💳': {\r\n en: ['credit card', 'banking', 'payment'],\r\n ru: ['кредитная карта', 'банковское дело', 'оплата']\r\n },\r\n '💎': {\r\n en: ['gem', 'diamond', 'valuable'],\r\n ru: ['драгоценный камень', 'бриллиант', 'ценный']\r\n },\r\n '⚖️': {\r\n en: ['balance scale', 'justice', 'law'],\r\n ru: ['весы', 'справедливость', 'закон']\r\n },\r\n '🪜': {\r\n en: ['ladder', 'climb', 'height'],\r\n ru: ['лестница', 'лазить', 'высота']\r\n },\r\n '🧰': {\r\n en: ['toolbox', 'tools', 'repair'],\r\n ru: ['ящик с инструментами', 'инструменты', 'ремонт']\r\n },\r\n '🔧': {\r\n en: ['wrench', 'repair', 'tools'],\r\n ru: ['гаечный ключ', 'ремонт', 'инструменты']\r\n },\r\n '🔨': {\r\n en: ['hammer', 'build', 'fix'],\r\n ru: ['молоток', 'строить', 'чинить']\r\n },\r\n '⚒️': {\r\n en: ['hammer and pick', 'construction', 'work'],\r\n ru: ['молот и кирка', 'строительство', 'работа']\r\n },\r\n '🛠️': {\r\n en: ['tools', 'fix', 'maintenance'],\r\n ru: ['инструменты', 'чинить', 'обслуживание']\r\n },\r\n '⛏️': {\r\n en: ['pickaxe', 'mining', 'digging'],\r\n ru: ['кирка', 'горное дело', 'копать']\r\n },\r\n\r\n // Writing & Reading\r\n '✏️': {\r\n en: ['pencil', 'writing', 'notes'],\r\n ru: ['карандаш', 'написание', 'заметки']\r\n },\r\n '🖊️': {\r\n en: ['pen', 'writing', 'signature'],\r\n ru: ['ручка', 'написание', 'подпись']\r\n },\r\n '🖋️': {\r\n en: ['fountain pen', 'calligraphy', 'writing'],\r\n ru: ['перьевая ручка', 'каллиграфия', 'написание']\r\n },\r\n '✒️': {\r\n en: ['nib', 'ink', 'writing'],\r\n ru: ['остриё', 'чернила', 'написание']\r\n },\r\n '🖌️': {\r\n en: ['paintbrush', 'art', 'painting'],\r\n ru: ['кисть', 'искусство', 'живопись']\r\n },\r\n '🖍️': {\r\n en: ['crayon', 'drawing', 'color'],\r\n ru: ['мелки', 'рисование', 'цвет']\r\n },\r\n '📝': {\r\n en: ['memo', 'notes', 'document'],\r\n ru: ['заметка', 'записи', 'документ']\r\n },\r\n '📚': {\r\n en: ['books', 'reading', 'library'],\r\n ru: ['книги', 'чтение', 'библиотека']\r\n },\r\n '📖': {\r\n en: ['open book', 'reading', 'story'],\r\n ru: ['открытая книга', 'чтение', 'история']\r\n },\r\n '🔖': {\r\n en: ['bookmark', 'reading', 'save'],\r\n ru: ['закладка', 'чтение', 'сохранить']\r\n },\r\n '📑': {\r\n en: ['bookmark tabs', 'pages', 'notes'],\r\n ru: ['закладки', 'страницы', 'заметки']\r\n },\r\n '🗒️': {\r\n en: ['spiral notepad', 'notes', 'journal'],\r\n ru: ['спиральный блокнот', 'записи', 'журнал']\r\n },\r\n '📄': {\r\n en: ['document', 'paper', 'page'],\r\n ru: ['документ', 'бумага', 'страница']\r\n },\r\n '📰': {\r\n en: ['newspaper', 'news', 'journalism'],\r\n ru: ['газета', 'новости', 'журналистика']\r\n },\r\n '🗞️': {\r\n en: ['rolled newspaper', 'press', 'print'],\r\n ru: ['свернутая газета', 'пресса', 'печатное издание']\r\n },\r\n '📁': {\r\n en: ['file folder', 'documents', 'storage'],\r\n ru: ['папка', 'документы', 'хранение']\r\n },\r\n '📂': {\r\n en: ['open folder', 'organization', 'files'],\r\n ru: ['открытая папка', 'организация', 'файлы']\r\n },\r\n '🗂️': {\r\n en: ['card index', 'catalog', 'records'],\r\n ru: ['карточный индекс', 'каталог', 'записи']\r\n },\r\n '🕐': {\r\n en: ['1 o\\'clock', 'clock face', 'time'],\r\n ru: ['час', 'циферблат', 'время']\r\n },\r\n '🕑': {\r\n en: ['2 o\\'clock', 'clock face', 'time'],\r\n ru: ['два часа', 'циферблат', 'время']\r\n },\r\n '🕒': {\r\n en: ['3 o\\'clock', 'clock face', 'time'],\r\n ru: ['три часа', 'циферблат', 'время']\r\n },\r\n '🕓': {\r\n en: ['4 o\\'clock', 'clock face', 'time'],\r\n ru: ['четыре часа', 'циферблат', 'время']\r\n },\r\n '🕔': {\r\n en: ['5 o\\'clock', 'clock face', 'time'],\r\n ru: ['пять часов', 'циферблат', 'время']\r\n },\r\n '🕕': {\r\n en: ['6 o\\'clock', 'clock face', 'time'],\r\n ru: ['шесть часов', 'циферблат', 'время']\r\n },\r\n '🕖': {\r\n en: ['7 o\\'clock', 'clock face', 'time'],\r\n ru: ['семь часов', 'циферблат', 'время']\r\n },\r\n '🕗': {\r\n en: ['8 o\\'clock', 'clock face', 'time'],\r\n ru: ['восемь часов', 'циферблат', 'время']\r\n },\r\n '🕘': {\r\n en: ['9 o\\'clock', 'clock face', 'time'],\r\n ru: ['девять часов', 'циферблат', 'время']\r\n },\r\n '🕙': {\r\n en: ['10 o\\'clock', 'clock face', 'time'],\r\n ru: ['десять часов', 'циферблат', 'время']\r\n },\r\n '🕚': {\r\n en: ['11 o\\'clock', 'clock face', 'time'],\r\n ru: ['одиннадцать часов', 'циферблат', 'время']\r\n },\r\n '🕛': {\r\n en: ['12 o\\'clock', 'clock face', 'time'],\r\n ru: ['двенадцать часов', 'циферблат', 'время']\r\n },\r\n '🕰️': {\r\n en: ['mantel clock', 'clock', 'time'],\r\n ru: ['настенные часы', 'часы', 'время']\r\n },\r\n '⏰': {\r\n en: ['alarm clock', 'wake up', 'time'],\r\n ru: ['будильник', 'пробуждение', 'время']\r\n },\r\n '⏱️': {\r\n en: ['stopwatch', 'timer', 'time'],\r\n ru: ['секундомер', 'таймер', 'время']\r\n },\r\n '⏲️': {\r\n en: ['kitchen timer', 'timer', 'time'],\r\n ru: ['кухонный таймер', 'таймер', 'время']\r\n },\r\n '⌚': {\r\n en: ['watch', 'smartwatch', 'time'],\r\n ru: ['часы', 'умные часы', 'время']\r\n },\r\n\r\n //------------------------- Symbols & Signs -------------------------\r\n // Hearts\r\n '❤️': {\r\n en: ['heart', 'love', 'affection'],\r\n ru: ['сердце', 'любовь', 'нежность']\r\n },\r\n '🧡': {\r\n en: ['orange heart', 'warmth', 'care'],\r\n ru: ['оранжевое сердце', 'теплота', 'забота']\r\n },\r\n '💛': {\r\n en: ['yellow heart', 'friendship', 'happiness'],\r\n ru: ['жёлтое сердце', 'дружба', 'счастье']\r\n },\r\n '💚': {\r\n en: ['green heart', 'envy', 'nature', 'eco'],\r\n ru: ['зелёное сердце', 'зависть', 'природа', 'эко']\r\n },\r\n '💙': {\r\n en: ['blue heart', 'loyalty', 'trust'],\r\n ru: ['синее сердце', 'верность', 'доверие']\r\n },\r\n '💜': {\r\n en: ['purple heart', 'compassion', 'admiration'],\r\n ru: ['фиолетовое сердце', 'сострадание', 'восхищение']\r\n },\r\n '🤎': {\r\n en: ['brown heart', 'earth', 'stability'],\r\n ru: ['коричневое сердце', 'земля', 'устойчивость']\r\n },\r\n '🖤': {\r\n en: ['black heart', 'mourning', 'dark'],\r\n ru: ['чёрное сердце', 'скорбь', 'тёмный']\r\n },\r\n '🤍': {\r\n en: ['white heart', 'pure', 'peace'],\r\n ru: ['белое сердце', 'чистый', 'мир']\r\n },\r\n '💔': {\r\n en: ['broken heart', 'sad', 'heartbreak'],\r\n ru: ['разбитое сердце', 'грусть', 'сердечная боль']\r\n },\r\n '❣️': {\r\n en: ['heart exclamation', 'love', 'emotion'],\r\n ru: ['сердечный восклицательный знак', 'любовь', 'эмоция']\r\n },\r\n '💕': {\r\n en: ['two hearts', 'romance', 'affection'],\r\n ru: ['два сердца', 'романтика', 'нежность']\r\n },\r\n '💞': {\r\n en: ['revolving hearts', 'love', 'relationship'],\r\n ru: ['вращающиеся сердца', 'любовь', 'отношения']\r\n },\r\n '💓': {\r\n en: ['beating heart', 'emotion', 'passion'],\r\n ru: ['бьющееся сердце', 'эмоция', 'страсть']\r\n },\r\n '💗': {\r\n en: ['growing heart', 'love', 'expanding'],\r\n ru: ['растущее сердце', 'любовь', 'расширяющееся']\r\n },\r\n '💖': {\r\n en: ['sparkling heart', 'admiration', 'shine'],\r\n ru: ['блестящее сердце', 'восхищение', 'сияние']\r\n },\r\n '💘': {\r\n en: ['heart with arrow', 'love', 'romance'],\r\n ru: ['сердце со стрелой', 'любовь', 'романтика']\r\n },\r\n '💝': {\r\n en: ['heart with ribbon', 'gift', 'affection'],\r\n ru: ['сердце с лентой', 'подарок', 'нежность']\r\n },\r\n '💟': {\r\n en: ['heart decoration', 'symbol', 'love'],\r\n ru: ['украшение сердца', 'символ', 'любовь']\r\n },\r\n\r\n // Religion\r\n '☮️': {\r\n en: ['peace', 'pacifism', 'symbol'],\r\n ru: ['мир', 'пацифизм', 'символ']\r\n },\r\n '✝️': {\r\n en: ['cross', 'christianity', 'religion'],\r\n ru: ['крест', 'христианство', 'религия']\r\n },\r\n '☪️': {\r\n en: ['star and crescent', 'islam', 'faith'],\r\n ru: ['звезда и полумесяц', 'ислам', 'вера']\r\n },\r\n '🕉️': {\r\n en: ['om', 'hinduism', 'spiritual'],\r\n ru: ['ом', 'индуизм', 'духовный']\r\n },\r\n '☸️': {\r\n en: ['dharma wheel', 'buddhism', 'karma'],\r\n ru: ['колесо дхармы', 'буддизм', 'карма']\r\n },\r\n '✡️': {\r\n en: ['star of david', 'judaism', 'faith'],\r\n ru: ['звезда Давида', 'иудаизм', 'вера']\r\n },\r\n '🔯': {\r\n en: ['hexagram', 'mystic', 'spiritual'],\r\n ru: ['шестиконечная звезда', 'мистический', 'духовный']\r\n },\r\n '🕎': {\r\n en: ['menorah', 'hanukkah', 'jewish'],\r\n ru: ['менора', 'Ханука', 'еврейский']\r\n },\r\n '☯️': {\r\n en: ['yin yang', 'balance', 'harmony'],\r\n ru: ['инь-ян', 'баланс', 'гармония']\r\n },\r\n '☦️': {\r\n en: ['orthodox cross', 'christianity', 'faith'],\r\n ru: ['православный крест', 'христианство', 'вера']\r\n },\r\n '🛐': {\r\n en: ['place of worship', 'religion', 'faith'],\r\n ru: ['место поклонения', 'религия', 'вера']\r\n },\r\n '⛎': {\r\n en: ['ophiuchus', 'zodiac', 'astrology'],\r\n ru: ['змееносец', 'зодиак', 'астрология']\r\n },\r\n\r\n // Warning & Restrictions\r\n '⚠️': {\r\n en: ['warning', 'caution', 'alert'],\r\n ru: ['предупреждение', 'осторожность', 'тревога']\r\n },\r\n '🚸': {\r\n en: ['children crossing', 'school', 'pedestrian'],\r\n ru: ['дети на переходе', 'школа', 'пешеход']\r\n },\r\n '⛔': {\r\n en: ['no entry', 'prohibited', 'restricted'],\r\n ru: ['вход запрещён', 'запрещено', 'ограничено']\r\n },\r\n '🚫': {\r\n en: ['prohibited', 'no', 'forbidden'],\r\n ru: ['запрещено', 'нет', 'запрещено']\r\n },\r\n '☢️': {\r\n en: ['radioactive', 'hazard', 'danger'],\r\n ru: ['радиоактивный', 'опасность', 'угроза']\r\n },\r\n '☣️': {\r\n en: ['biohazard', 'toxic', 'warning'],\r\n ru: ['биологическая опасность', 'ядовитый', 'предупреждение']\r\n },\r\n\r\n // Math Symbols\r\n '➕': {\r\n en: ['plus', 'addition', 'math'],\r\n ru: ['плюс', 'сложение', 'математика']\r\n },\r\n '➖': {\r\n en: ['minus', 'subtraction', 'math'],\r\n ru: ['минус', 'вычитание', 'математика']\r\n },\r\n '➗': {\r\n en: ['division', 'divide', 'math'],\r\n ru: ['деление', 'делить', 'математика']\r\n },\r\n '✖️': {\r\n en: ['multiplication', 'times', 'math'],\r\n ru: ['умножение', 'раз', 'математика']\r\n },\r\n '♾️': {\r\n en: ['infinity', 'limitless', 'math'],\r\n ru: ['бесконечность', 'безграничный', 'математика']\r\n },\r\n '💲': {\r\n en: ['dollar', 'money', 'currency'],\r\n ru: ['доллар', 'деньги', 'валюта']\r\n },\r\n '💱': {\r\n en: ['currency exchange', 'finance', 'money'],\r\n ru: ['обмен валюты', 'финансы', 'деньги']\r\n },\r\n\r\n // Arrows\r\n '⬆️': {\r\n en: ['up arrow', 'increase', 'direction'],\r\n ru: ['стрелка вверх', 'увеличение', 'направление']\r\n },\r\n '↗️': {\r\n en: ['up-right arrow', 'growth', 'move'],\r\n ru: ['стрелка вверх-вправо', 'рост', 'движение']\r\n },\r\n '➡️': {\r\n en: ['right arrow', 'next', 'forward'],\r\n ru: ['стрелка вправо', 'далее', 'вперёд']\r\n },\r\n '↘️': {\r\n en: ['down-right arrow', 'decrease', 'move'],\r\n ru: ['стрелка вниз-вправо', 'уменьшение', 'движение']\r\n },\r\n '⬇️': {\r\n en: ['down arrow', 'lower', 'decline'],\r\n ru: ['стрелка вниз', 'понижение', 'снижение']\r\n },\r\n '↙️': {\r\n en: ['down-left arrow', 'falling', 'move'],\r\n ru: ['стрелка вниз-влево', 'падение', 'движение']\r\n },\r\n '⬅️': {\r\n en: ['left arrow', 'back', 'previous'],\r\n ru: ['стрелка влево', 'назад', 'предыдущий']\r\n },\r\n '↖️': {\r\n en: ['up-left arrow', 'direction', 'move'],\r\n ru: ['стрелка вверх-влево', 'направление', 'движение']\r\n },\r\n '↕️': {\r\n en: ['vertical arrows', 'up down', 'change'],\r\n ru: ['вертикальные стрелки', 'вверх вниз', 'изменение']\r\n },\r\n '↔️': {\r\n en: ['horizontal arrows', 'left right', 'switch'],\r\n ru: ['горизонтальные стрелки', 'влево вправо', 'переключение']\r\n },\r\n '↩️': {\r\n en: ['back arrow', 'undo', 'return'],\r\n ru: ['стрелка назад', 'отмена', 'возврат']\r\n },\r\n '↪️': {\r\n en: ['right curved arrow', 'redirect', 'turn'],\r\n ru: ['изогнутая стрелка вправо', 'перенаправление', 'поворот']\r\n },\r\n '⤴️': {\r\n en: ['up-right arrow', 'diagonal', 'move'],\r\n ru: ['диагональная стрелка вверх-вправо', 'диагональ', 'движение']\r\n },\r\n '⤵️': {\r\n en: ['down-right arrow', 'diagonal', 'move'],\r\n ru: ['диагональная стрелка вниз-вправо', 'диагональ', 'движение']\r\n },\r\n '🔃': {\r\n en: ['repeat', 'cycle', 'refresh'],\r\n ru: ['повтор', 'цикл', 'обновление']\r\n },\r\n '🔄': {\r\n en: ['counterclockwise arrows', 'reload', 'sync'],\r\n ru: ['стрелки против часовой стрелки', 'перезагрузка', 'синхронизация']\r\n },\r\n\r\n // Miscellaneous Symbols\r\n '🔆': {\r\n en: ['bright', 'high brightness', 'sun'],\r\n ru: ['яркий', 'высокая яркость', 'солнце']\r\n },\r\n '📶': {\r\n en: ['signal', 'network', 'connection'],\r\n ru: ['сигнал', 'сеть', 'соединение']\r\n },\r\n '🎦': {\r\n en: ['cinema', 'movies', 'entertainment'],\r\n ru: ['кино', 'фильмы', 'развлечения']\r\n },\r\n '🔅': {\r\n en: ['dim', 'low brightness', 'light'],\r\n ru: ['тусклый', 'низкая яркость', 'свет']\r\n },\r\n '♻️': {\r\n en: ['recycle', 'eco', 'sustainability'],\r\n ru: ['переработка', 'эко', 'устойчивость']\r\n },\r\n '✅': {\r\n en: ['check mark', 'yes', 'approved'],\r\n ru: ['галочка', 'да', 'одобрено']\r\n },\r\n '❌': {\r\n en: ['cross mark', 'no', 'wrong'],\r\n ru: ['крестик', 'нет', 'неправильно']\r\n },\r\n '❎': {\r\n en: ['negative cross', 'decline', 'cancel'],\r\n ru: ['отрицательный крест', 'отказ', 'отмена']\r\n },\r\n '➰': {\r\n en: ['curly loop', 'infinity', 'twist'],\r\n ru: ['извилистая петля', 'бесконечность', 'скрутка']\r\n },\r\n '➿': {\r\n en: ['double curly loop', 'loop', 'repeat'],\r\n ru: ['двойная извилистая петля', 'петля', 'повтор']\r\n },\r\n '〽️': {\r\n en: ['part alternation', 'music', 'symbol'],\r\n ru: ['знак чередования', 'музыка', 'символ']\r\n },\r\n '✳️': {\r\n en: ['eight-spoked asterisk', 'star', 'highlight'],\r\n ru: ['восьмиконечная астериска', 'звезда', 'акцент']\r\n },\r\n '✴️': {\r\n en: ['eight-pointed star', 'shine', 'special'],\r\n ru: ['восьмиконечная звезда', 'сияние', 'особый']\r\n },\r\n '❇️': {\r\n en: ['sparkle', 'highlight', 'shine'],\r\n ru: ['сверкание', 'акцент', 'сияние']\r\n },\r\n '©️': {\r\n en: ['copyright', 'legal', 'rights'],\r\n ru: ['авторское право', 'юридический', 'права']\r\n },\r\n '®️': {\r\n en: ['registered', 'trademark', 'brand'],\r\n ru: ['зарегистрировано', 'торговая марка', 'бренд']\r\n },\r\n '™️': {\r\n en: ['trademark', 'brand', 'symbol'],\r\n ru: ['торговая марка', 'бренд', 'символ']\r\n },\r\n\r\n //------------------------- Flags -------------------------\r\n // Special Flags\r\n '🏁': {\r\n en: ['checkered flag', 'finish line', 'racing'],\r\n ru: ['шашечный флаг', 'финишная черта', 'гонки']\r\n },\r\n '🚩': {\r\n en: ['triangular flag', 'mark', 'warning'],\r\n ru: ['треугольный флаг', 'метка', 'предупреждение']\r\n },\r\n '🎌': {\r\n en: ['crossed flags', 'celebration', 'Japan'],\r\n ru: ['перекрещённые флаги', 'празднование', 'Япония']\r\n },\r\n '🏴': {\r\n en: ['black flag', 'protest', 'symbol'],\r\n ru: ['чёрный флаг', 'протест', 'символ']\r\n },\r\n '🏳️': {\r\n en: ['white flag', 'surrender', 'peace'],\r\n ru: ['белый флаг', 'капитуляция', 'мир']\r\n },\r\n '🏳️🌈': {\r\n en: ['rainbow flag', 'LGBTQ+', 'pride'],\r\n ru: ['радужный флаг', 'ЛГБТК+', 'гордость']\r\n },\r\n '🏳️⚧️': {\r\n en: ['transgender flag', 'trans', 'pride'],\r\n ru: ['трансгендерный флаг', 'транс', 'гордость']\r\n },\r\n '🏴☠️': {\r\n en: ['pirate flag', 'skull', 'danger'],\r\n ru: ['пиратский флаг', 'череп', 'опасность']\r\n },\r\n\r\n // Country Flags (Sample)\r\n '🇺🇸': {\r\n en: ['United States', 'USA', 'America'],\r\n ru: ['Соединённые Штаты', 'США', 'Америка']\r\n },\r\n '🇬🇧': {\r\n en: ['United Kingdom', 'UK', 'Britain'],\r\n ru: ['Соединённое Королевство', 'Великобритания', 'Британия']\r\n },\r\n '🇯🇵': {\r\n en: ['Japan', 'Japanese', 'Asia'],\r\n ru: ['Япония', 'японский', 'Азия']\r\n },\r\n '🇰🇷': {\r\n en: ['South Korea', 'Korea', 'Asian'],\r\n ru: ['Южная Корея', 'Корея', 'азиатский']\r\n },\r\n '🇩🇪': {\r\n en: ['Germany', 'Deutschland', 'Europe'],\r\n ru: ['Германия', 'Германия', 'Европа']\r\n },\r\n '🇨🇳': {\r\n en: ['China', 'Chinese', 'Asia'],\r\n ru: ['Китай', 'китайский', 'Азия']\r\n },\r\n '🇧🇷': {\r\n en: ['Brazil', 'Brasil', 'South America'],\r\n ru: ['Бразилия', 'Бразилия', 'Южная Америка']\r\n },\r\n '🇮🇳': {\r\n en: ['India', 'Indian', 'Asia'],\r\n ru: ['Индия', 'индийский', 'Азия']\r\n },\r\n '🇫🇷': {\r\n en: ['France', 'French', 'Europe'],\r\n ru: ['Франция', 'французский', 'Европа']\r\n },\r\n '🇪🇸': {\r\n en: ['Spain', 'Spanish', 'Europe'],\r\n ru: ['Испания', 'испанский', 'Европа']\r\n },\r\n '🇮🇹': {\r\n en: ['Italy', 'Italian', 'Europe'],\r\n ru: ['Италия', 'итальянский', 'Европа']\r\n },\r\n '🇷🇺': {\r\n en: ['Russia', 'Russian', 'Europe'],\r\n ru: ['Россия', 'русский', 'Европа']\r\n },\r\n '🇨🇦': {\r\n en: ['Canada', 'Canadian', 'North America'],\r\n ru: ['Канада', 'канадский', 'Северная Америка']\r\n },\r\n '🇦🇺': {\r\n en: ['Australia', 'Aussie', 'Oceania'],\r\n ru: ['Австралия', 'австралиец', 'Океания']\r\n },\r\n '🇳🇿': {\r\n en: ['New Zealand', 'Kiwi', 'Oceania'],\r\n ru: ['Новая Зеландия', 'киви', 'Океания']\r\n },\r\n // Additional Country Flags (Extended)\r\n '🇲🇽': {\r\n en: ['Mexico', 'Mexican', 'Latin America'],\r\n ru: ['Мексика', 'мексиканский', 'Латинская Америка']\r\n },\r\n '🇦🇷': {\r\n en: ['Argentina', 'Argentinian', 'South America'],\r\n ru: ['Аргентина', 'аргентинский', 'Южная Америка']\r\n },\r\n '🇵🇰': {\r\n en: ['Pakistan', 'Pakistani', 'Asia'],\r\n ru: ['Пакистан', 'пакистанский', 'Азия']\r\n },\r\n '🇪🇬': {\r\n en: ['Egypt', 'Egyptian', 'Africa'],\r\n ru: ['Египет', 'египетский', 'Африка']\r\n },\r\n '🇸🇪': {\r\n en: ['Sweden', 'Swedish', 'Europe'],\r\n ru: ['Швеция', 'шведский', 'Европа']\r\n },\r\n '🇳🇴': {\r\n en: ['Norway', 'Norwegian', 'Europe'],\r\n ru: ['Норвегия', 'норвежский', 'Европа']\r\n },\r\n '🇳🇱': {\r\n en: ['Netherlands', 'Dutch', 'Europe'],\r\n ru: ['Нидерланды', 'голландский', 'Европа']\r\n },\r\n '🇨🇭': {\r\n en: ['Switzerland', 'Swiss', 'Europe'],\r\n ru: ['Швейцария', 'швейцарский', 'Европа']\r\n },\r\n '🇹🇷': {\r\n en: ['Turkey', 'Turkish', 'Eurasia'],\r\n ru: ['Турция', 'турецкий', 'Евразия']\r\n },\r\n '🇮🇩': {\r\n en: ['Indonesia', 'Indonesian', 'Asia'],\r\n ru: ['Индонезия', 'индонезийский', 'Азия']\r\n },\r\n '🇸🇬': {\r\n en: ['Singapore', 'Singaporean', 'Asia'],\r\n ru: ['Сингапур', 'сингапурский', 'Азия']\r\n },\r\n '🇮🇱': {\r\n en: ['Israel', 'Israeli', 'Middle East'],\r\n ru: ['Израиль', 'израильский', 'Ближний Восток']\r\n },\r\n '🇵🇹': {\r\n en: ['Portugal', 'Portuguese', 'Europe'],\r\n ru: ['Португалия', 'португальский', 'Европа']\r\n },\r\n '🇵🇱': {\r\n en: ['Poland', 'Polish', 'Europe'],\r\n ru: ['Польша', 'польский', 'Европа']\r\n },\r\n '🇹🇭': {\r\n en: ['Thailand', 'Thai', 'Asia'],\r\n ru: ['Таиланд', 'тайский', 'Азия']\r\n }\r\n};\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/data/emojiData.js?");
/***/ }),
/***/ "./src/definitions.js":
/*!****************************!*\
!*** ./src/definitions.js ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ BASE_URL: () => (/* binding */ BASE_URL),\n/* harmony export */ GAME_URL: () => (/* binding */ GAME_URL),\n/* harmony export */ XMPP_BIND_URL: () => (/* binding */ XMPP_BIND_URL),\n/* harmony export */ config: () => (/* binding */ config),\n/* harmony export */ delay: () => (/* binding */ delay),\n/* harmony export */ emojiFaces: () => (/* binding */ emojiFaces),\n/* harmony export */ reconnectionDelay: () => (/* binding */ reconnectionDelay),\n/* harmony export */ state: () => (/* binding */ state),\n/* harmony export */ trustedDomains: () => (/* binding */ trustedDomains),\n/* harmony export */ userListDelay: () => (/* binding */ userListDelay)\n/* harmony export */ });\n/* harmony import */ var _src_styles_style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../src/styles/style.css */ \"./src/styles/style.css\");\n // styles\r\n\r\n// URL constants\r\nconst BASE_URL = 'https://klavogonki.ru';\r\nconst GAME_URL = `${BASE_URL}/g/?gmid=`;\r\nconst XMPP_BIND_URL = `${BASE_URL}/xmpp-httpbind/`;\r\n\r\n// Sleep time (ms)\r\nconst delay = 200;\r\nconst userListDelay = 5000;\r\nconst reconnectionDelay = 5000;\r\n\r\n// Configuration with localStorage integration\r\nconst config = {\r\n get username() {\r\n const data = localStorage.getItem('klavoauth');\r\n return data ? JSON.parse(data).username : '';\r\n },\r\n get password() {\r\n const data = localStorage.getItem('klavoauth');\r\n return data ? JSON.parse(data).password : '';\r\n }\r\n};\r\n\r\nconst emojiFaces = [\r\n // People Emojis (Facial expressions)\r\n '😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆',\r\n '😉', '😊', '😋', '😎', '😏', '😐', '😑', '😒',\r\n '😓', '😔', '😕', '😖', '😗', '😘', '😙', '😚',\r\n '😜', '😝', '😛', '🤑', '🤗', '🤔', '🤐', '🤨',\r\n '😣', '😥', '😮', '🤯', '😳', '😱', '😨', '😰',\r\n '😢', '🤪', '😵', '😲', '🤤', '😷', '🤒', '🤕',\r\n '🤢', '🤧', '😇', '🥳', '🥺', '😬', '😴', '😌',\r\n '🤥', '🥴', '🥵', '🥶', '🤧', '🤭', '🤫', '😠',\r\n '😡', '😳', '😞', '😟', '😕',\r\n\r\n // Cat Emojis (Expressive faces of cats)\r\n '🐱', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾',\r\n\r\n // Other Animal Emojis (Various animals' faces)\r\n '🐶', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',\r\n '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵',\r\n '🙈', '🙉', '🙊', '🐔', '🦄'\r\n];\r\n\r\nlet state = {\r\n bigImageEvents: {}\r\n};\r\n\r\n// List of trusted domains\r\nconst trustedDomains = [\r\n 'klavogonki.ru',\r\n 'youtube.com', // youtube main\r\n 'youtu.be', // youtube share\r\n 'imgur.com',\r\n 'pikabu.ru',\r\n 'userapi.com', // vk.com\r\n 'ibb.co', // imgbb.com\r\n 'yaplakal.com',\r\n 'freepik.com'\r\n];\n\n//# sourceURL=webpack://tampermonkey-script/./src/definitions.js?");
/***/ }),
/***/ "./src/events.js":
/*!***********************!*\
!*** ./src/events.js ***!
\***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ setupDragHandlers: () => (/* binding */ setupDragHandlers),\n/* harmony export */ setupResizeHandlers: () => (/* binding */ setupResizeHandlers),\n/* harmony export */ setupWindowResizeHandler: () => (/* binding */ setupWindowResizeHandler)\n/* harmony export */ });\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./helpers.js */ \"./src/helpers.js\");\n\r\n\r\n// ------------------------- Drag Handlers (Floating) -------------------------\r\nlet isDragging = false,\r\n dragStartX,\r\n dragStartY,\r\n dragStartLeft,\r\n dragStartTop;\r\n\r\nfunction setupDragHandlers() {\r\n document.addEventListener('mousedown', (e) => {\r\n const dragArea = e.target.closest('.chat-drag-area');\r\n if (!dragArea) return;\r\n\r\n const chat = document.getElementById('app-chat-container');\r\n let chatState = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.getChatState)();\r\n\r\n isDragging = true;\r\n dragStartX = e.clientX;\r\n dragStartY = e.clientY;\r\n dragStartLeft = chat.offsetLeft;\r\n // Use the computed top if no inline style exists\r\n dragStartTop = parseInt(chat.style.top) || chat.getBoundingClientRect().top;\r\n \r\n // If the chat isn’t already floating, convert it now\r\n if (!chatState.floating) {\r\n const newTop = window.innerHeight - chat.offsetHeight;\r\n chat.style.top = newTop + 'px';\r\n chat.style.bottom = '';\r\n chatState.top = newTop;\r\n chatState.floating = true;\r\n chat.classList.add(\"floating-chat\");\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.saveChatState)(chatState);\r\n }\r\n\r\n document.body.style.userSelect = 'none';\r\n });\r\n\r\n document.addEventListener('mousemove', (e) => {\r\n if (!isDragging) return;\r\n\r\n const chat = document.getElementById('app-chat-container');\r\n const viewportWidth = window.innerWidth;\r\n const viewportHeight = window.innerHeight;\r\n const deltaX = e.clientX - dragStartX;\r\n const deltaY = e.clientY - dragStartY;\r\n const newLeft = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(dragStartLeft + deltaX, 0, viewportWidth - chat.offsetWidth);\r\n // Ensure the chat does not go above the viewport (>= 0) or below viewport bottom\r\n const newTop = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(dragStartTop + deltaY, 0, viewportHeight - chat.offsetHeight);\r\n \r\n chat.style.left = newLeft + 'px';\r\n chat.style.top = newTop + 'px';\r\n \r\n let chatState = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.getChatState)();\r\n chatState.left = newLeft;\r\n chatState.top = newTop;\r\n chatState.floating = true;\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.saveChatState)(chatState);\r\n });\r\n\r\n document.addEventListener('mouseup', () => {\r\n if (!isDragging) return;\r\n isDragging = false;\r\n\r\n const chat = document.getElementById('app-chat-container');\r\n const viewportWidth = window.innerWidth;\r\n const viewportHeight = window.innerHeight;\r\n const chatRect = chat.getBoundingClientRect();\r\n const SNAP_THRESHOLD = 50;\r\n \r\n const outOfBounds = chatRect.left < 0 || chatRect.top < 0 ||\r\n chatRect.right > viewportWidth || chatRect.bottom > viewportHeight;\r\n const nearBottom = (viewportHeight - chatRect.bottom) < SNAP_THRESHOLD;\r\n \r\n let chatState = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.getChatState)();\r\n if (outOfBounds || nearBottom) {\r\n // Snap to bottom when out of bounds\r\n chat.style.top = '';\r\n chat.style.bottom = '0';\r\n chatState.floating = false;\r\n chat.classList.remove(\"floating-chat\");\r\n } else {\r\n chatState.floating = true;\r\n chatState.top = chatRect.top;\r\n chat.classList.add(\"floating-chat\");\r\n }\r\n \r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.saveChatState)(chatState);\r\n document.body.style.userSelect = '';\r\n });\r\n}\r\n\r\n// ------------------------- Resize Handlers -------------------------\r\nlet isResizing = false,\r\n resizeType = null,\r\n startX,\r\n startY,\r\n startWidth,\r\n startHeight,\r\n startLeft,\r\n startTop,\r\n offsetY = 0; // used to keep the cursor's relative position on the edge\r\n\r\nfunction setupResizeHandlers() {\r\n document.addEventListener('mousedown', (e) => {\r\n const handle = e.target.closest('.resize-handle');\r\n if (!handle) return;\r\n isResizing = true;\r\n resizeType = handle.classList[1]; // e.g., 'top', 'left', 'right'\r\n const chat = document.getElementById('app-chat-container');\r\n startX = e.clientX;\r\n startY = e.clientY;\r\n startWidth = chat.offsetWidth;\r\n startHeight = chat.offsetHeight;\r\n startLeft = chat.offsetLeft;\r\n // Use inline style top if available, otherwise the computed top\r\n startTop = parseInt(chat.style.top) || chat.getBoundingClientRect().top;\r\n offsetY = e.clientY - startTop; // capture the cursor offset relative to chat's top edge\r\n document.body.style.userSelect = 'none';\r\n });\r\n\r\n document.addEventListener('mousemove', (e) => {\r\n if (!isResizing) return;\r\n const chat = document.getElementById('app-chat-container');\r\n const viewportWidth = window.innerWidth;\r\n const viewportHeight = window.innerHeight;\r\n const computedStyle = getComputedStyle(document.documentElement);\r\n const minWidth = parseInt(computedStyle.getPropertyValue('--min-chat-width')) || 250;\r\n const minHeight = parseInt(computedStyle.getPropertyValue('--min-chat-height')) || 200;\r\n let chatState = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.getChatState)();\r\n\r\n // Horizontal resizing (applies for left and right handles)\r\n if (resizeType === 'left') {\r\n const newWidth = Math.max(minWidth, startWidth - (e.clientX - startX));\r\n const newLeft = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(startLeft + (e.clientX - startX), 0, viewportWidth - newWidth);\r\n chat.style.width = newWidth + 'px';\r\n chat.style.left = newLeft + 'px';\r\n chatState.width = newWidth;\r\n chatState.left = newLeft;\r\n } else if (resizeType === 'right') {\r\n const maxWidth = viewportWidth - chat.getBoundingClientRect().left;\r\n const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidth + (e.clientX - startX)));\r\n chat.style.width = newWidth + 'px';\r\n chatState.width = newWidth;\r\n }\r\n\r\n // Vertical resizing for the three handles will use the cursor’s Y position.\r\n // Calculate the desired new top edge based on the original offset.\r\n const newTopCandidate = e.clientY - offsetY;\r\n\r\n // For the top handle, the chat’s top edge should follow the cursor.\r\n if (resizeType === 'top') {\r\n if (chatState.floating === false) {\r\n // Docked: bottom remains anchored at viewport bottom.\r\n // When docked, the computed startTop equals (viewportHeight - startHeight)\r\n let newTop = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(newTopCandidate, 0, viewportHeight - minHeight);\r\n let newHeight = viewportHeight - newTop; // bottom is fixed at viewport bottom\r\n chat.style.top = newTop + 'px';\r\n chat.style.height = newHeight + 'px';\r\n chatState.top = newTop;\r\n chatState.height = newHeight;\r\n } else {\r\n // Floating: simply follow the cursor for the top edge,\r\n // ensuring we don’t shrink below minHeight.\r\n let newTop = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(newTopCandidate, 0, startTop + startHeight - minHeight);\r\n let newHeight = startHeight - (newTop - startTop);\r\n // Also do not allow height to exceed viewport height\r\n newHeight = Math.min(newHeight, viewportHeight);\r\n chat.style.top = newTop + 'px';\r\n chat.style.height = newHeight + 'px';\r\n chatState.top = newTop;\r\n chatState.height = newHeight;\r\n }\r\n }\r\n \r\n // For left/right handles, we also want vertical resizing.\r\n if (resizeType === 'left' || resizeType === 'right') {\r\n if (chatState.floating === false) {\r\n // Docked: bottom is fixed\r\n let newTop = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(newTopCandidate, 0, viewportHeight - minHeight);\r\n let newHeight = viewportHeight - newTop;\r\n chat.style.top = newTop + 'px';\r\n chat.style.height = newHeight + 'px';\r\n chatState.top = newTop;\r\n chatState.height = newHeight;\r\n } else {\r\n // Floating: follow the cursor while keeping at least minHeight.\r\n let newTop = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.clamp)(newTopCandidate, 0, startTop + startHeight - minHeight);\r\n let newHeight = startHeight - (newTop - startTop);\r\n newHeight = Math.min(newHeight, viewportHeight - newTop);\r\n chat.style.top = newTop + 'px';\r\n chat.style.height = newHeight + 'px';\r\n chatState.top = newTop;\r\n chatState.height = newHeight;\r\n }\r\n }\r\n\r\n // Ensure height never exceeds the viewport height\r\n if (chat.offsetHeight > viewportHeight) {\r\n chat.style.height = viewportHeight + 'px';\r\n if (chatState.floating === false) {\r\n chat.style.top = '0px'; // if docked, force top to 0 so height = viewport height\r\n chatState.top = 0;\r\n }\r\n chatState.height = viewportHeight;\r\n }\r\n \r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.saveChatState)(chatState);\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.handleElementsBehavior)();\r\n });\r\n\r\n document.addEventListener('mouseup', () => {\r\n isResizing = false;\r\n document.body.style.userSelect = '';\r\n });\r\n}\r\n\r\nfunction setupWindowResizeHandler() {\r\n window.addEventListener('resize', () => {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.restoreChatState)();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.handleElementsBehavior)();\r\n });\r\n}\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/events.js?");
/***/ }),
/***/ "./src/helpers.js":
/*!************************!*\
!*** ./src/helpers.js ***!
\************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ addBigImageEventListeners: () => (/* binding */ addBigImageEventListeners),\n/* harmony export */ addViewportMeta: () => (/* binding */ addViewportMeta),\n/* harmony export */ adjustVisibility: () => (/* binding */ adjustVisibility),\n/* harmony export */ applyFontSize: () => (/* binding */ applyFontSize),\n/* harmony export */ calibrateToMoscowTime: () => (/* binding */ calibrateToMoscowTime),\n/* harmony export */ checkImageExists: () => (/* binding */ checkImageExists),\n/* harmony export */ clamp: () => (/* binding */ clamp),\n/* harmony export */ createFontSizeControl: () => (/* binding */ createFontSizeControl),\n/* harmony export */ createLengthPopup: () => (/* binding */ createLengthPopup),\n/* harmony export */ decodeURL: () => (/* binding */ decodeURL),\n/* harmony export */ enterPrivateMode: () => (/* binding */ enterPrivateMode),\n/* harmony export */ exitPrivateMode: () => (/* binding */ exitPrivateMode),\n/* harmony export */ extractCleanUsername: () => (/* binding */ extractCleanUsername),\n/* harmony export */ extractTargetUsername: () => (/* binding */ extractTargetUsername),\n/* harmony export */ extractUserId: () => (/* binding */ extractUserId),\n/* harmony export */ fetchJSON: () => (/* binding */ fetchJSON),\n/* harmony export */ focusTextInput: () => (/* binding */ focusTextInput),\n/* harmony export */ getAuthData: () => (/* binding */ getAuthData),\n/* harmony export */ getChatState: () => (/* binding */ getChatState),\n/* harmony export */ getExactUserIdByName: () => (/* binding */ getExactUserIdByName),\n/* harmony export */ getRandomEmojiAvatar: () => (/* binding */ getRandomEmojiAvatar),\n/* harmony export */ getRandomInterval: () => (/* binding */ getRandomInterval),\n/* harmony export */ handleElementsBehavior: () => (/* binding */ handleElementsBehavior),\n/* harmony export */ handlePrivateMessageInput: () => (/* binding */ handlePrivateMessageInput),\n/* harmony export */ highlightMentionWords: () => (/* binding */ highlightMentionWords),\n/* harmony export */ initChatLengthPopupEvents: () => (/* binding */ initChatLengthPopupEvents),\n/* harmony export */ isEncodedURL: () => (/* binding */ isEncodedURL),\n/* harmony export */ isTrustedDomain: () => (/* binding */ isTrustedDomain),\n/* harmony export */ mentionColors: () => (/* binding */ mentionColors),\n/* harmony export */ observeMessagesPanel: () => (/* binding */ observeMessagesPanel),\n/* harmony export */ parseMessageText: () => (/* binding */ parseMessageText),\n/* harmony export */ parseUsername: () => (/* binding */ parseUsername),\n/* harmony export */ privateMessageState: () => (/* binding */ privateMessageState),\n/* harmony export */ removeBigImageEventListeners: () => (/* binding */ removeBigImageEventListeners),\n/* harmony export */ restoreChatState: () => (/* binding */ restoreChatState),\n/* harmony export */ restoreFontSize: () => (/* binding */ restoreFontSize),\n/* harmony export */ saveChatState: () => (/* binding */ saveChatState),\n/* harmony export */ scrollToBottom: () => (/* binding */ scrollToBottom),\n/* harmony export */ setupPrivateMessageEvents: () => (/* binding */ setupPrivateMessageEvents),\n/* harmony export */ setupRandomEmojiAttention: () => (/* binding */ setupRandomEmojiAttention),\n/* harmony export */ showChatAlert: () => (/* binding */ showChatAlert),\n/* harmony export */ sleep: () => (/* binding */ sleep),\n/* harmony export */ usernameColors: () => (/* binding */ usernameColors)\n/* harmony export */ });\n/* harmony import */ var _converters_image_converter_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./converters/image-converter.js */ \"./src/converters/image-converter.js\");\n/* harmony import */ var _converters_video_converter_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./converters/video-converter.js */ \"./src/converters/video-converter.js\");\n/* harmony import */ var _definitions_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./definitions.js */ \"./src/definitions.js\");\n/* harmony import */ var _icons_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./icons.js */ \"./src/icons.js\");\n/* harmony import */ var _animations_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./animations.js */ \"./src/animations.js\");\n/* harmony import */ var _data_emojiData_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./data/emojiData.js */ \"./src/data/emojiData.js\");\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\nconst getAuthData = () => {\r\n const pageData = JSON.parse([...document.scripts]\r\n .find(s => s.text.includes('PageData'))\r\n ?.text.match(/\\.constant\\('PageData', ({[\\s\\S]*?})\\)/)?.[1]\r\n .replace(/(\\w+):/g, '\"$1\":').replace(/'/g, '\"') || '{}');\r\n\r\n return pageData?.chatParams && {\r\n username: `${pageData.chatParams.user.id}#${pageData.chatParams.user.login}`,\r\n password: pageData.chatParams.pass\r\n };\r\n};\r\n\r\nfunction colorGenerator(config) {\r\n // Load stored mapping from sessionStorage (if available) or initialize an empty object.\r\n const storedMapping = sessionStorage.getItem('usernameColors');\r\n const hueMap = storedMapping ? JSON.parse(storedMapping) : {};\r\n\r\n return {\r\n hueStep: config.hueStep || 30,\r\n maxHue: config.maxHue || 360,\r\n saturation: config.saturation || '80%',\r\n lightness: config.lightness || '50%',\r\n\r\n getColor(username) {\r\n // Normalize the username to ensure consistency.\r\n const key = username.trim().toLowerCase();\r\n\r\n // Return the color if it already exists.\r\n if (hueMap[key]) {\r\n return hueMap[key];\r\n }\r\n\r\n // Otherwise, generate a new color.\r\n const maxSteps = this.maxHue / this.hueStep;\r\n const hue = Math.floor(Math.random() * maxSteps) * this.hueStep;\r\n const color = `hsl(${hue}, ${this.saturation}, ${this.lightness})`;\r\n hueMap[key] = color;\r\n\r\n // Save the updated mapping back to sessionStorage.\r\n sessionStorage.setItem('usernameColors', JSON.stringify(hueMap));\r\n return color;\r\n }\r\n };\r\n}\r\n\r\nconst usernameColors = colorGenerator({\r\n maxHue: 210,\r\n hueStep: 30,\r\n saturation: '80%',\r\n lightness: '50%'\r\n});\r\n\r\nconst mentionColors = colorGenerator({\r\n maxHue: 210,\r\n hueStep: 30,\r\n saturation: '80%',\r\n lightness: '50%'\r\n});\r\n\r\nlet lastEmojiAvatar = null;\r\nfunction getRandomEmojiAvatar() {\r\n let newEmoji;\r\n do {\r\n newEmoji = _definitions_js__WEBPACK_IMPORTED_MODULE_2__.emojiFaces[Math.floor(Math.random() * _definitions_js__WEBPACK_IMPORTED_MODULE_2__.emojiFaces.length)];\r\n } while (newEmoji === lastEmojiAvatar);\r\n lastEmojiAvatar = newEmoji;\r\n return newEmoji;\r\n}\r\n\r\nfunction handleElementsBehavior() {\r\n const wrapper = document.querySelector('#app-chat-container .chat-wrapper');\r\n if (!wrapper) return;\r\n const chatContainer = document.querySelector('#app-chat-container');\r\n const isNarrow = wrapper.offsetWidth <= 780;\r\n const isVeryNarrow = wrapper.offsetWidth <= 380;\r\n const isExtremelyNarrow = wrapper.offsetWidth <= 340;\r\n const isMaximized = chatContainer.classList.contains('maximized');\r\n const userList = document.querySelector('#app-chat-container .user-list-container');\r\n\r\n let isUserListOpen = false;\r\n\r\n if (userList) {\r\n if (isNarrow && !isMaximized) {\r\n userList.style.position = 'absolute';\r\n userList.style.height = '100%';\r\n userList.style.top = '0';\r\n userList.style.right = '0';\r\n userList.style.transition = 'transform 0.3s ease';\r\n userList.style.zIndex = '1001';\r\n userList.style.transform = 'translateX(100%)';\r\n\r\n let revealButton = document.querySelector('#app-chat-container .reveal-userlist-btn');\r\n if (!revealButton) {\r\n revealButton = document.createElement('button');\r\n revealButton.className = 'reveal-userlist-btn hidden-userlist';\r\n revealButton.textContent = '📋';\r\n\r\n chatContainer.appendChild(revealButton);\r\n\r\n function closeUserList(event) {\r\n if (!userList.contains(event.target) && event.target !== revealButton) {\r\n userList.style.transform = 'translateX(100%)';\r\n revealButton.classList.remove('shown-userlist');\r\n revealButton.classList.add('hidden-userlist');\r\n isUserListOpen = false;\r\n document.removeEventListener('click', closeUserList, true);\r\n }\r\n }\r\n\r\n revealButton.addEventListener('click', (event) => {\r\n event.stopPropagation();\r\n if (!isUserListOpen) {\r\n userList.style.transform = 'translateX(0)';\r\n revealButton.classList.remove('hidden-userlist');\r\n revealButton.classList.add('shown-userlist');\r\n isUserListOpen = true;\r\n\r\n setTimeout(() => {\r\n document.addEventListener('click', closeUserList, true);\r\n }, 10);\r\n }\r\n });\r\n }\r\n } else {\r\n userList.style.position = '';\r\n userList.style.height = '';\r\n userList.style.top = '';\r\n userList.style.right = '';\r\n userList.style.transform = '';\r\n userList.style.zIndex = '';\r\n\r\n const revealButton = document.querySelector('#app-chat-container .reveal-userlist-btn');\r\n if (revealButton) {\r\n revealButton.remove();\r\n }\r\n }\r\n }\r\n\r\n document.querySelectorAll('#app-chat-container .message').forEach(msg => {\r\n msg.style.flexDirection = (isNarrow && !isMaximized) ? 'column' : 'row';\r\n msg.style.marginBottom = (isNarrow && !isMaximized) ? '0.4em' : '0';\r\n });\r\n\r\n document.querySelectorAll('#app-chat-container .video-container').forEach(video => {\r\n if (isExtremelyNarrow) {\r\n video.style.transformOrigin = 'left';\r\n video.style.transform = 'scale(0.8)';\r\n } else if (isVeryNarrow) {\r\n video.style.transformOrigin = 'left';\r\n video.style.transform = 'scale(0.9)';\r\n } else {\r\n video.style.transformOrigin = '';\r\n video.style.transform = '';\r\n }\r\n });\r\n}\r\n\r\nfunction observeMessagesPanel() {\r\n const messagesPanel = document.getElementById('messages-panel');\r\n if (!messagesPanel) return;\r\n\r\n const observer = new MutationObserver(() => {\r\n handleElementsBehavior();\r\n (0,_converters_video_converter_js__WEBPACK_IMPORTED_MODULE_1__.convertVideoLinksToPlayer)();\r\n (0,_converters_image_converter_js__WEBPACK_IMPORTED_MODULE_0__.convertImageLinksToImage)();\r\n scrollToBottom();\r\n });\r\n\r\n observer.observe(messagesPanel, { childList: true, subtree: true });\r\n}\r\n\r\nfunction restoreChatState() {\r\n const chat = document.getElementById('app-chat-container');\r\n const toggleButton = document.querySelector('.chat-toggle-button');\r\n if (!chat || !toggleButton) return;\r\n\r\n const state = getChatState();\r\n const viewportWidth = window.innerWidth;\r\n const viewportHeight = window.innerHeight;\r\n const computedStyle = getComputedStyle(document.documentElement);\r\n const minWidth = parseInt(computedStyle.getPropertyValue('--min-chat-width')) || 250;\r\n const minHeight = parseInt(computedStyle.getPropertyValue('--min-chat-height')) || 200;\r\n\r\n chat.style.width = Math.min(viewportWidth, Math.max(minWidth, state.width)) + 'px';\r\n chat.style.height = Math.min(viewportHeight, Math.max(minHeight, state.height)) + 'px';\r\n chat.style.left = clamp(state.left, 0, viewportWidth - chat.offsetWidth) + 'px';\r\n\r\n if (state.floating) {\r\n chat.style.top = clamp(state.top, 0, viewportHeight - chat.offsetHeight) + 'px';\r\n chat.style.bottom = '';\r\n chat.classList.add(\"floating-chat\");\r\n chat.style.display = state.isVisible ? 'flex' : 'none';\r\n chat.style.opacity = state.isVisible ? '1' : '0';\r\n toggleButton.innerHTML = state.isVisible ? _icons_js__WEBPACK_IMPORTED_MODULE_3__.closeSVG : _icons_js__WEBPACK_IMPORTED_MODULE_3__.openSVG;\r\n } else {\r\n chat.style.bottom = '0';\r\n chat.style.top = '';\r\n chat.classList.remove(\"floating-chat\");\r\n chat.classList.remove('visible-chat', 'hidden-chat');\r\n chat.classList.add(state.isVisible ? 'visible-chat' : 'hidden-chat');\r\n toggleButton.innerHTML = state.isVisible ? _icons_js__WEBPACK_IMPORTED_MODULE_3__.closeSVG : _icons_js__WEBPACK_IMPORTED_MODULE_3__.openSVG;\r\n }\r\n\r\n handleElementsBehavior();\r\n}\r\n\r\n// 1. Update helpers.js to include the font size in the getChatState function\r\n\r\n// In getChatState() function in helpers.js\r\nfunction getChatState() {\r\n const savedState = localStorage.getItem('chatState');\r\n const defaultState = {\r\n height: 300,\r\n width: Math.min(window.innerWidth, 600),\r\n left: 0,\r\n floating: false,\r\n top: window.innerHeight - 300,\r\n isVisible: true,\r\n fontSizeMultiplier: 1.0 // Add the new default font size multiplier\r\n };\r\n\r\n return savedState ? { ...defaultState, ...JSON.parse(savedState) } : defaultState;\r\n}\r\n\r\n// 2. Add a function to apply font size changes\r\nfunction applyFontSize(multiplier) {\r\n const chatContainer = document.getElementById('app-chat-container');\r\n const messageInput = document.getElementById('message-input');\r\n if (!chatContainer) return;\r\n\r\n // Apply font size to the main container\r\n chatContainer.style.fontSize = `${multiplier}em`;\r\n\r\n // Apply base font size to message input without multiplier\r\n // since it inherits the multiplier from the container\r\n if (messageInput) {\r\n messageInput.style.fontSize = '1em';\r\n }\r\n\r\n // Save the current multiplier in the chat state\r\n const chatState = getChatState();\r\n saveChatState({\r\n ...chatState,\r\n fontSizeMultiplier: multiplier\r\n });\r\n}\r\n\r\n// 3. Add a function to restore font size from state on initialization\r\nfunction restoreFontSize() {\r\n const chatState = getChatState();\r\n applyFontSize(chatState.fontSizeMultiplier);\r\n}\r\n\r\n// 4. Function to create the font size slider\r\nfunction createFontSizeControl() {\r\n const chatContainer = document.getElementById('app-chat-container');\r\n if (!chatContainer) return;\r\n\r\n const chatState = getChatState();\r\n\r\n // Create font size control container\r\n const fontSizeControl = document.createElement('div');\r\n fontSizeControl.className = 'font-size-control';\r\n\r\n // Create the slider\r\n const fontSlider = document.createElement('input');\r\n fontSlider.type = 'range';\r\n fontSlider.min = '0.8';\r\n fontSlider.max = '1.5';\r\n fontSlider.step = '0.1';\r\n fontSlider.value = chatState.fontSizeMultiplier;\r\n fontSlider.className = 'font-size-slider';\r\n\r\n // Prevent dragging the chat when interacting with the slider\r\n fontSlider.addEventListener('mousedown', (e) => {\r\n e.stopPropagation();\r\n });\r\n\r\n // Update font size on input change\r\n fontSlider.addEventListener('input', (e) => {\r\n const value = parseFloat(e.target.value);\r\n applyFontSize(value);\r\n });\r\n\r\n // Append the slider to the control container\r\n fontSizeControl.appendChild(fontSlider);\r\n\r\n // Add the control container to the chat drag area\r\n const dragArea = document.querySelector('.chat-drag-area');\r\n if (dragArea) {\r\n dragArea.appendChild(fontSizeControl);\r\n }\r\n}\r\n\r\nfunction saveChatState(state) {\r\n localStorage.setItem('chatState', JSON.stringify(state));\r\n}\r\n\r\nconst parseMessageText = text => {\r\n let i = 0, urls = [];\r\n text = text.replace(/(\\b(https?|ftp):\\/\\/[-A-Z0-9+&@#\\/%?=~_|!:,.;]*[-A-Z0-9+&@#\\/%=~_|])/ig, m => {\r\n urls.push(m);\r\n return `___URL${i++}___`;\r\n });\r\n text = text\r\n .replace(/:(\\w+):/g, (_, e) => `<img src=\"https://klavogonki.ru/img/smilies/${e}.gif\" alt=\"${e}\" />`)\r\n .replace(/(\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F)/gu, '<span class=\"emoji-adjuster\">$&</span>');\r\n urls.forEach((url, idx) => {\r\n text = text.replace(`___URL${idx}___`, `<a href=\"${url}\" target=\"_blank\">${url}</a>`);\r\n });\r\n return text;\r\n}\r\n\r\nfunction clamp(value, min, max) {\r\n return Math.min(Math.max(value, min), max);\r\n}\r\n\r\nfunction parseUsername(username) {\r\n if (typeof username !== 'string') return username;\r\n return username.replace(/^\\d+#/, '');\r\n}\r\n\r\n// Extract userId from JID, handling formats like \"123456#Username\"\r\nfunction extractUserId(jid) {\r\n if (!jid) return null;\r\n const parts = jid.split('/');\r\n if (parts.length < 2) return null;\r\n\r\n const secondPart = parts[1];\r\n return secondPart.split('#')[0]; // Get everything before the # character\r\n}\r\n\r\n// Extract clean username from the full JID or login string\r\nfunction extractCleanUsername(login) {\r\n if (!login) return \"Unknown\";\r\n\r\n // If login contains #, get everything after it\r\n if (login.includes('#')) {\r\n return login.split('#')[1];\r\n }\r\n\r\n // Replace the parseUsername call with direct implementation\r\n return login.replace(/^\\d+#/, '');\r\n}\r\n\r\nfunction addBigImageEventListeners() {\r\n Object.entries(_definitions_js__WEBPACK_IMPORTED_MODULE_2__.state.bigImageEvents).forEach(([event, handler]) => {\r\n document.addEventListener(event, handler);\r\n });\r\n}\r\n\r\nfunction removeBigImageEventListeners() {\r\n Object.entries(_definitions_js__WEBPACK_IMPORTED_MODULE_2__.state.bigImageEvents).forEach(([event, handler]) => {\r\n document.removeEventListener(event, handler);\r\n });\r\n}\r\n\r\nfunction adjustVisibility(element, action, opacity) {\r\n if (!element) return;\r\n void element.offsetHeight; // Force reflow\r\n element.style.transition = 'opacity 0.3s ease';\r\n element.style.opacity = action === 'show' ? opacity : '0';\r\n if (action === 'hide') {\r\n element.addEventListener('transitionend', () => {\r\n if (element.style.opacity === '0' && element.parentNode) {\r\n element.parentNode.removeChild(element);\r\n }\r\n }, { once: true });\r\n }\r\n}\r\n\r\nconst isTrustedDomain = url => {\r\n try {\r\n const { hostname } = new URL(url);\r\n const domain = hostname.toLowerCase().split('.').slice(-2).join('.');\r\n return { isTrusted: _definitions_js__WEBPACK_IMPORTED_MODULE_2__.trustedDomains.includes(domain), domain };\r\n } catch (err) {\r\n console.error(\"Error in isTrustedDomain:\", err.message);\r\n return { isTrusted: false, domain: url };\r\n }\r\n};\r\n\r\nfunction isEncodedURL(url) {\r\n const urlPattern = /^https?:\\/\\//;\r\n const encodedPattern = /%[0-9A-Fa-f]{2}/;\r\n return urlPattern.test(url) && encodedPattern.test(url);\r\n}\r\n\r\nfunction decodeURL(url) {\r\n const [base] = url.split('#');\r\n return decodeURIComponent(base).replace(/ /g, '_');\r\n}\r\n\r\nfunction highlightMentionWords() {\r\n const container = document.getElementById('messages-panel');\r\n if (!container) return;\r\n\r\n const storedKeywords = localStorage.getItem('mentionKeywords');\r\n if (!storedKeywords) return;\r\n\r\n let mentionKeywords;\r\n try {\r\n mentionKeywords = JSON.parse(storedKeywords);\r\n if (!Array.isArray(mentionKeywords)) return;\r\n } catch (e) {\r\n return;\r\n }\r\n\r\n const globalProcessed = new WeakSet();\r\n const messages = container.querySelectorAll('.message-text');\r\n\r\n messages.forEach((message) => {\r\n const walker = document.createTreeWalker(\r\n message,\r\n NodeFilter.SHOW_TEXT,\r\n {\r\n acceptNode: (node) => {\r\n if (globalProcessed.has(node)) return NodeFilter.FILTER_SKIP;\r\n const parent = node.parentElement;\r\n if (parent.closest('.mention, .time, .username')) {\r\n return NodeFilter.FILTER_SKIP;\r\n }\r\n return NodeFilter.FILTER_ACCEPT;\r\n }\r\n }\r\n );\r\n\r\n const nodes = [];\r\n let currentNode;\r\n while ((currentNode = walker.nextNode())) nodes.push(currentNode);\r\n\r\n nodes.forEach((node) => {\r\n if (!globalProcessed.has(node)) {\r\n processNode(node, mentionKeywords);\r\n globalProcessed.add(node);\r\n }\r\n });\r\n });\r\n\r\n function processNode(node, keywords) {\r\n const regex = /(@?[\\wа-яА-ЯёЁ'-]+)|[\\s]+|[^@\\s\\wа-яА-ЯёЁ'-]+/gu;\r\n const tokens = node.textContent.match(regex) || [];\r\n\r\n const fragment = document.createDocumentFragment();\r\n\r\n tokens.forEach(token => {\r\n const isMatch = keywords.some(keyword =>\r\n keyword.localeCompare(token, undefined, { sensitivity: 'accent' }) === 0\r\n );\r\n\r\n if (isMatch) {\r\n const mentionSpan = document.createElement('span');\r\n mentionSpan.className = 'mention';\r\n token.split('').forEach(char => {\r\n const charSpan = document.createElement('span');\r\n charSpan.style.color = mentionColors.getColor(char);\r\n charSpan.textContent = char;\r\n mentionSpan.appendChild(charSpan);\r\n });\r\n fragment.appendChild(mentionSpan);\r\n } else {\r\n fragment.appendChild(document.createTextNode(token));\r\n }\r\n });\r\n\r\n node.parentNode.replaceChild(fragment, node);\r\n }\r\n}\r\n\r\nlet firstTime = true;\r\nconst scrollThreshold = 600;\r\n\r\nfunction scrollToBottom() {\r\n const container = document.getElementById('messages-panel');\r\n if (!container) return;\r\n\r\n if (firstTime) {\r\n container.scrollTop = container.scrollHeight;\r\n firstTime = false;\r\n } else {\r\n const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;\r\n if (distanceFromBottom <= scrollThreshold) {\r\n container.scrollTop = container.scrollHeight;\r\n }\r\n }\r\n}\r\n\r\nfunction showChatAlert(message, options = {}) {\r\n const dragArea = document.querySelector('.chat-drag-area');\r\n if (!dragArea) return;\r\n\r\n const existingAlert = dragArea.querySelector('.chat-dynamic-alert');\r\n if (existingAlert && existingAlert.parentNode === dragArea) {\r\n dragArea.removeChild(existingAlert);\r\n }\r\n\r\n const defaultOptions = { type: 'info', duration: 3000 };\r\n const settings = { ...defaultOptions, ...options };\r\n\r\n const colorMap = {\r\n info: '#2196F3',\r\n warning: '#FF9800',\r\n error: '#F44336',\r\n success: '#4CAF50'\r\n };\r\n\r\n const alertElement = document.createElement('div');\r\n alertElement.className = 'chat-dynamic-alert';\r\n alertElement.innerHTML = message;\r\n\r\n alertElement.style.cssText = `\r\n position: absolute;\r\n white-space: nowrap;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n color: ${colorMap[settings.type] || colorMap.info};\r\n padding: 5px 10px;\r\n border-radius: 3px;\r\n z-index: 1000;\r\n font-family: \"Montserrat\", sans-serif;\r\n font-size: 10px;\r\n font-weight: 500;\r\n opacity: 0;\r\n `;\r\n\r\n dragArea.appendChild(alertElement);\r\n\r\n function animateAlert() {\r\n requestAnimationFrame(() => {\r\n alertElement.style.transition = 'all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55)';\r\n alertElement.style.opacity = '1';\r\n alertElement.style.transform = 'translate(-50%, -50%)';\r\n\r\n setTimeout(() => {\r\n alertElement.style.transition = 'transform 0.05s ease-in-out';\r\n const shakeSequence = [\r\n { x: 5, delay: 0 },\r\n { x: -7, delay: 50 },\r\n { x: 9, delay: 100 },\r\n { x: -6, delay: 150 },\r\n { x: 4, delay: 200 },\r\n { x: -3, delay: 250 },\r\n { x: 0, delay: 300 }\r\n ];\r\n\r\n shakeSequence.forEach((move) => {\r\n setTimeout(() => {\r\n alertElement.style.transform = `translate(calc(-50% + ${move.x}px), -50%)`;\r\n }, move.delay);\r\n });\r\n }, 300);\r\n\r\n setTimeout(() => {\r\n alertElement.style.transition = 'opacity 0.3s ease-in-out';\r\n alertElement.style.opacity = '0';\r\n setTimeout(() => {\r\n if (alertElement && alertElement.parentNode === dragArea) {\r\n dragArea.removeChild(alertElement);\r\n }\r\n }, 300);\r\n }, settings.duration);\r\n });\r\n }\r\n\r\n animateAlert();\r\n}\r\n\r\nfunction focusTextInput() {\r\n const chatContainer = document.getElementById('app-chat-container');\r\n const element = document.getElementById('message-input');\r\n if (element && chatContainer && chatContainer.style.display !== 'none') {\r\n element.focus();\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\n// Helper to fetch JSON and validate response\r\nasync function fetchJSON(url) {\r\n const response = await fetch(url);\r\n if (!response.ok) throw new Error(`Failed to fetch ${url}`);\r\n return response.json();\r\n}\r\n\r\n// Helper function to get Exact user ID by username via the search API\r\nasync function getExactUserIdByName(userName) {\r\n // Define the search API URL\r\n const searchApiUrl = `https://klavogonki.ru/api/profile/search-users?query=${encodeURIComponent(userName)}`;\r\n\r\n try {\r\n // Get search results from the API\r\n const searchResults = await fetchJSON(searchApiUrl);\r\n\r\n // Ensure search results exist and contain data\r\n if (!searchResults.all?.length) {\r\n throw new Error(`User ${userName} not found.`);\r\n }\r\n\r\n // Return the ID of the user with the exact matching login\r\n const user = searchResults.all.find(user => user.login === userName);\r\n if (!user) {\r\n throw new Error(`Exact match for user ${userName} not found.`);\r\n }\r\n\r\n return user.id;\r\n } catch (error) {\r\n // console.error('Error getting user ID:', error);\r\n showChatAlert(`Could not find user \"${userName}\"`, { type: 'error', duration: 5000 });\r\n return null;\r\n }\r\n}\r\n\r\n// Function to extract target username from message input\r\nfunction extractTargetUsername(input) {\r\n const match = input.match(/<([^>]+)>/);\r\n return match ? match[1] : null;\r\n}\r\n\r\n// State management for private messaging\r\nconst privateMessageState = {\r\n isPrivateMode: false,\r\n targetUsername: null,\r\n targetId: null,\r\n fullJid: null,\r\n\r\n async setPrivateTarget(username) {\r\n if (!username) {\r\n this.exitPrivateMode();\r\n return false;\r\n }\r\n\r\n try {\r\n const userId = await getExactUserIdByName(username);\r\n if (!userId) return false;\r\n\r\n this.isPrivateMode = true;\r\n this.targetUsername = username;\r\n this.targetId = userId;\r\n this.fullJid = `${userId}#${username}@jabber.klavogonki.ru/web`;\r\n\r\n return true;\r\n } catch (error) {\r\n console.error('Error setting private target:', error);\r\n return false;\r\n }\r\n },\r\n\r\n exitPrivateMode() {\r\n this.isPrivateMode = false;\r\n this.targetUsername = null;\r\n this.targetId = null;\r\n this.fullJid = null;\r\n }\r\n};\r\n\r\n// Toggle private message mode based on input value\r\nasync function handlePrivateMessageInput(inputElement) {\r\n if (!inputElement) return;\r\n const input = inputElement.value;\r\n // Updated regex to include hyphens and other common username special characters\r\n const privateModeRegex = /^\\/pm\\s+([\\wа-яА-ЯёЁ\\-\\.\\_\\+]+)\\s/;\r\n const exitPrivateModeRegex = /^\\/exit\\s*$/;\r\n const match = input.match(privateModeRegex);\r\n if (match) {\r\n const username = match[1];\r\n const success = await privateMessageState.setPrivateTarget(username);\r\n if (success) {\r\n enterPrivateMode(username);\r\n inputElement.value = input.replace(privateModeRegex, ''); // Remove the /pm username part\r\n } else {\r\n showChatAlert(`Could not find user \"${username}\"`, { type: 'error', duration: 3000 });\r\n exitPrivateMode();\r\n }\r\n } else if (exitPrivateModeRegex.test(input)) {\r\n exitPrivateMode();\r\n inputElement.value = ''; // Clear the input\r\n }\r\n}\r\n\r\nfunction enterPrivateMode(username) {\r\n const messageInput = document.getElementById('message-input');\r\n if (privateMessageState.isPrivateMode && privateMessageState.targetUsername !== username) {\r\n exitPrivateMode();\r\n }\r\n\r\n if (!messageInput.classList.contains('private-mode') || privateMessageState.targetUsername !== username) {\r\n messageInput.classList.add('private-mode');\r\n messageInput.placeholder = `PM to ➡ ${username}`;\r\n\r\n // Create or update exit button\r\n let exitButton = document.querySelector('.private-mode-exit');\r\n if (!exitButton) {\r\n exitButton = document.createElement('span');\r\n exitButton.className = 'private-mode-exit';\r\n\r\n // Add click event to exit private mode\r\n exitButton.addEventListener('click', () => {\r\n exitPrivateMode();\r\n messageInput.focus();\r\n });\r\n\r\n // Add the exit button to the UI near the input\r\n const inputContainer = messageInput.parentElement;\r\n inputContainer.insertBefore(exitButton, messageInput.nextSibling);\r\n }\r\n\r\n // Set default closed lock emoji and title\r\n exitButton.innerHTML = \"🔒\";\r\n exitButton.title = \"Exit private mode\";\r\n\r\n // Change emoji on hover: open lock on mouseenter, closed lock on mouseleave\r\n exitButton.addEventListener('mouseenter', () => {\r\n exitButton.innerHTML = \"🔓\";\r\n });\r\n\r\n exitButton.addEventListener('mouseleave', () => {\r\n exitButton.innerHTML = \"🔒\";\r\n });\r\n\r\n showChatAlert(`Private chat with ${username} activated`, { type: 'warning', duration: 3000 });\r\n privateMessageState.isPrivateMode = true;\r\n privateMessageState.targetUsername = username;\r\n } else if (privateMessageState.targetUsername === username) {\r\n messageInput.placeholder = `️PM to ➡ ${username}`;\r\n showChatAlert(`Private chat with ${username} activated`, { type: 'warning', duration: 3000 });\r\n }\r\n}\r\n\r\nfunction exitPrivateMode() {\r\n const messageInput = document.getElementById('message-input');\r\n if (messageInput.classList.contains('private-mode')) {\r\n messageInput.classList.remove('private-mode');\r\n messageInput.placeholder = ''; // Reset placeholder\r\n\r\n // Remove the exit button\r\n const exitButton = document.querySelector('.private-mode-exit');\r\n if (exitButton) exitButton.remove();\r\n\r\n privateMessageState.exitPrivateMode();\r\n showChatAlert('Exited private chat mode', { type: 'success', duration: 3000 });\r\n }\r\n}\r\n\r\n// Handle ESC key to exit private mode\r\nfunction setupPrivateMessageEvents() {\r\n const input = document.getElementById('message-input');\r\n if (!input) return;\r\n\r\n // Add ESC key handler to exit private mode\r\n input.addEventListener('keydown', (e) => {\r\n if (e.key === 'Escape' && privateMessageState.isPrivateMode) {\r\n exitPrivateMode();\r\n e.preventDefault();\r\n }\r\n });\r\n\r\n // Check for private message mode on input changes\r\n input.addEventListener('input', () => {\r\n handlePrivateMessageInput(input);\r\n });\r\n}\r\n\r\n// Only the sleep function is exported from here.\r\nfunction sleep(ms) {\r\n return new Promise(resolve => setTimeout(resolve, ms));\r\n}\r\n\r\n// Helper function to check if an image exists\r\nfunction checkImageExists(url) {\r\n return new Promise((resolve) => {\r\n const img = new Image();\r\n img.onload = () => resolve(true);\r\n img.onerror = () => resolve(false);\r\n img.src = url;\r\n });\r\n}\r\n\r\n// Function to randomize emoji and add shake effect\r\nfunction setupRandomEmojiAttention(emojiButton, frequency) {\r\n // Get all emoji keys from the emojiKeywords object\r\n const emojis = Object.keys(_data_emojiData_js__WEBPACK_IMPORTED_MODULE_5__.emojiKeywords);\r\n\r\n // Original emoji to return to after attention-getting effect\r\n const originalEmoji = \"🙂\";\r\n\r\n // Function to select random emoji and apply shake effect\r\n const showRandomEmoji = () => {\r\n // Get a random emoji from the collection\r\n const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)];\r\n\r\n // Set the random emoji\r\n emojiButton.innerHTML = randomEmoji;\r\n\r\n // Apply shake effect\r\n (0,_animations_js__WEBPACK_IMPORTED_MODULE_4__.addShakeEffect)(emojiButton);\r\n\r\n // Return to original emoji after animation completes\r\n setTimeout(() => {\r\n emojiButton.innerHTML = originalEmoji;\r\n }, 1500);\r\n };\r\n\r\n // Set interval to periodically show random emoji\r\n const intervalId = setInterval(showRandomEmoji, frequency);\r\n\r\n // Store the intervalId on the element for potential cleanup\r\n emojiButton.randomEmojiIntervalId = intervalId;\r\n\r\n return intervalId;\r\n}\r\n\r\n/**\r\n * Generates a random number of milliseconds between the specified minimum and maximum values\r\n * @param {number} minMs - Minimum time in milliseconds\r\n * @param {number} maxMs - Maximum time in milliseconds\r\n * @returns {number} - A random number of milliseconds between minMs and maxMs (inclusive)\r\n */\r\nfunction getRandomInterval(minMs, maxMs) {\r\n // Ensure min is not greater than max\r\n if (minMs > maxMs) {\r\n [minMs, maxMs] = [maxMs, minMs]; // Swap values if min > max\r\n }\r\n\r\n // Calculate a random value between min and max (inclusive)\r\n return Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;\r\n}\r\n\r\nfunction addViewportMeta() {\r\n if (!document.querySelector('meta[name=\"viewport\"]')) {\r\n const viewportMeta = document.createElement('meta');\r\n viewportMeta.name = 'viewport';\r\n viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';\r\n document.head.appendChild(viewportMeta);\r\n console.log('Viewport meta tag added dynamically');\r\n }\r\n}\r\n\r\n// Instead of getting the elements immediately, we declare module-level variables.\r\nlet chatField = null;\r\nlet messagesContainer = null;\r\nlet lengthPopup = null;\r\n\r\n/**\r\n * Create and append the length popup.\r\n * @param {HTMLElement} container - The container (messagesPanel) to which the popup should be appended.\r\n */\r\nfunction createLengthPopup(container) {\r\n messagesContainer = container;\r\n lengthPopup = document.createElement('div');\r\n lengthPopup.className = 'length-field-popup';\r\n messagesContainer.appendChild(lengthPopup);\r\n}\r\n\r\n// Create a canvas for text measurement.\r\nconst textMeasurementCanvas = document.createElement('canvas');\r\nconst textMeasurementContext = textMeasurementCanvas.getContext('2d');\r\n\r\nlet isPopupVisible = false;\r\nlet previousLength = 0;\r\nlet hidePopupTimeout;\r\n\r\nfunction updateLengthPopupColor(length) {\r\n if (!lengthPopup) {\r\n console.error('lengthPopup is not defined');\r\n return;\r\n }\r\n let textColor;\r\n if (length === 0) {\r\n textColor = 'hsl(200, 20%, 50%)'; // Light Blue\r\n } else if (length >= 1 && length <= 90) {\r\n textColor = 'hsl(120, 100%, 40%)'; // Bright Green\r\n } else if (length > 90 && length <= 100) {\r\n const factor = (length - 90) / 10;\r\n const h = Math.round(120 + factor * (60 - 120)); // Interpolating hue\r\n textColor = `hsl(${h}, 100%, 40%)`;\r\n } else if (length > 100 && length <= 190) {\r\n textColor = 'hsl(60, 100%, 50%)'; // Bright Yellow\r\n } else if (length > 190 && length <= 200) {\r\n const factor = (length - 190) / 10;\r\n const h = Math.round(60 + factor * (30 - 60)); // Interpolating hue\r\n textColor = `hsl(${h}, 100%, 50%)`;\r\n } else if (length > 200 && length <= 250) {\r\n textColor = 'hsl(40, 100%, 50%)'; // Orange\r\n } else if (length > 250 && length <= 300) {\r\n const factor = (length - 250) / 50;\r\n const h = Math.round(40 + factor * (0 - 40)); // Interpolating hue\r\n textColor = `hsl(${h}, 100%, 70%)`;\r\n } else {\r\n textColor = 'hsl(0, 100%, 70%)'; // Red\r\n }\r\n lengthPopup.style.color = textColor;\r\n}\r\n\r\nfunction updatePopupMetrics(text) {\r\n if (!chatField) {\r\n console.error('chatField is not set.');\r\n return;\r\n }\r\n // Get current font from input field.\r\n const computedStyle = getComputedStyle(chatField);\r\n textMeasurementContext.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;\r\n // Measure text.\r\n const textWidth = textMeasurementContext.measureText(text).width;\r\n // Calculate position.\r\n const newLeft = chatField.offsetLeft + textWidth + 5;\r\n const maxLeft = chatField.offsetLeft + chatField.offsetWidth - lengthPopup.offsetWidth;\r\n lengthPopup.style.left = `${Math.min(newLeft, maxLeft)}px`;\r\n}\r\n\r\nconst arrowRightBold = \"➡\"; // Heavy right arrow\r\nconst arrowLeftBold = \"⬅\"; // Heavy left arrow\r\n\r\nfunction updateLengthPopup(length) {\r\n let displayText =\r\n length > previousLength ? `${length} ${arrowRightBold}` :\r\n length < previousLength ? `${arrowLeftBold} ${length}` :\r\n `${length}`;\r\n \r\n lengthPopup.textContent = displayText;\r\n updateLengthPopupColor(length);\r\n previousLength = length;\r\n}\r\n\r\nfunction togglePopup(show) {\r\n if (isPopupVisible === show) return;\r\n lengthPopup.classList.toggle('bounce-in', show);\r\n lengthPopup.classList.toggle('bounce-out', !show);\r\n isPopupVisible = show;\r\n if (!show) setTimeout(() => lengthPopup.classList.remove('bounce-out'), 500);\r\n}\r\n\r\nfunction resetPopup() {\r\n updateLengthPopup(0);\r\n Object.assign(lengthPopup.style, { left: '0px', color: 'hsl(200, 20%, 50%)' });\r\n}\r\n\r\nfunction handleInputEvent() {\r\n clearTimeout(hidePopupTimeout);\r\n updateLengthPopup(chatField.value.length);\r\n updatePopupMetrics(chatField.value);\r\n togglePopup(true);\r\n hidePopupTimeout = setTimeout(() => togglePopup(false), 1000);\r\n}\r\n\r\nfunction handleKeydownEvent(e) {\r\n if (e.key !== 'Enter') return;\r\n resetPopup();\r\n togglePopup(true);\r\n hidePopupTimeout = setTimeout(() => togglePopup(false), 1000);\r\n}\r\n\r\n/**\r\n * Initializes chat length popup events.\r\n * @param {HTMLElement} field - The chat input field.\r\n */\r\nfunction initChatLengthPopupEvents(field) {\r\n chatField = field;\r\n if (!chatField) {\r\n console.error('chatField is null');\r\n return;\r\n }\r\n // Only attach event listeners if the popup was created.\r\n if (!lengthPopup) return;\r\n chatField.addEventListener('input', handleInputEvent);\r\n chatField.addEventListener('keydown', handleKeydownEvent);\r\n}\r\n\r\n/**\r\n * Converts a given local time to Moscow time (UTC+3) based on the system's timezone.\r\n *\r\n * How it works:\r\n * 1. Gets the system's local timezone offset in minutes (positive if behind UTC).\r\n * 2. Converts the local offset to total minutes from UTC.\r\n * 3. Defines Moscow's fixed offset as UTC+3 (180 minutes).\r\n * 4. Calculates the difference between Moscow's offset and the local offset.\r\n * 5. Parses the input time and converts it into total minutes since midnight.\r\n * 6. Adjusts the time by the calculated difference.\r\n * 7. Ensures the result stays within the 24-hour format (wrap-around handling).\r\n * 8. Converts the result back to HH:MM:SS format and returns it.\r\n *\r\n * @param {string} time - The local time in \"HH:MM:SS\" format.\r\n * @returns {string} - The converted time in Moscow time (HH:MM:SS).\r\n */\r\nfunction calibrateToMoscowTime(time) {\r\n // Get local timezone offset in minutes (positive if local is behind UTC)\r\n const localOffsetMinutes = new Date().getTimezoneOffset();\r\n\r\n // Convert local offset to total minutes from UTC (local time = UTC + localTotalOffset)\r\n const localTotalOffset = -localOffsetMinutes;\r\n\r\n // Moscow is UTC+3 (180 minutes)\r\n const moscowOffset = 3 * 60; // 180 minutes\r\n\r\n // Calculate the adjustment needed: Moscow offset - local offset\r\n const diffMinutes = moscowOffset - localTotalOffset;\r\n\r\n // Parse input time\r\n const [hours, minutes, seconds] = time.split(':').map(Number);\r\n\r\n // Convert input time to total minutes since 00:00\r\n const totalInputMinutes = hours * 60 + minutes;\r\n\r\n // Adjust by diff and wrap within a single day (1440 minutes)\r\n let adjustedMinutes = totalInputMinutes + diffMinutes;\r\n adjustedMinutes = ((adjustedMinutes % 1440) + 1440) % 1440; // Ensure positive\r\n\r\n // Convert back to hours and minutes\r\n const adjustedHours = Math.floor(adjustedMinutes / 60);\r\n const adjustedMins = adjustedMinutes % 60;\r\n\r\n // Format the result with original seconds\r\n return `${adjustedHours.toString().padStart(2, '0')}:` +\r\n `${adjustedMins.toString().padStart(2, '0')}:` +\r\n `${seconds.toString().padStart(2, '0')}`;\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/helpers.js?");
/***/ }),
/***/ "./src/icons.js":
/*!**********************!*\
!*** ./src/icons.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ closeSVG: () => (/* binding */ closeSVG),\n/* harmony export */ collapseSVG: () => (/* binding */ collapseSVG),\n/* harmony export */ expandSVG: () => (/* binding */ expandSVG),\n/* harmony export */ helpSVG: () => (/* binding */ helpSVG),\n/* harmony export */ openSVG: () => (/* binding */ openSVG),\n/* harmony export */ sendSVG: () => (/* binding */ sendSVG)\n/* harmony export */ });\nconst svgUrl = \"http://www.w3.org/2000/svg\";\r\nconst iconSize = 24;\r\n\r\n// SVG icon\r\nconst buttonAccentColdColor = \"#096AD9\";\r\nconst buttonAccentWarmColor = \"#deb887\";\r\nconst buttonOpenedColor = \"#82B32A\";\r\nconst buttonClosedColor = \"#B34A2A\";\r\nconst sendSVG = `\r\n <svg xmlns=\"${svgUrl}\" \r\n width=\"${iconSize}\" \r\n height=\"${iconSize}\" \r\n viewBox=\"0 0 250 250\">\r\n <path fill=\"${buttonAccentColdColor}\" d=\"M22.32 98.04l-19.04 -87.15c-0.75,-3.46 0.48,-6.84 3.29,-9 2.81,-2.17 6.39,-2.49 9.55,-0.87l225.95 116.02c3.07,1.57 4.87,4.52 4.87,7.96 0,3.44 -1.8,6.39 -4.87,7.96l-225.95 116.02c-3.16,1.62 -6.74,1.3 -9.55,-0.87 -2.81,-2.16 -4.04,-5.54 -3.29,-9l19.04 -87.15c0.79,-3.62 3.53,-6.26 7.18,-6.91l102.6 -18.19c0.91,-0.16 1.56,-0.94 1.56,-1.86 0,-0.92 -0.65,-1.7 -1.56,-1.86l-102.6 -18.19c-3.65,-0.65 -6.39,-3.29 -7.18,-6.91z\"/>\r\n </svg>\r\n`;\r\n\r\nconst closeSVG = `\r\n <svg xmlns=\"${svgUrl}\" \r\n width=\"${iconSize / 1.6}\" \r\n height=\"${iconSize / 1.6}\" \r\n viewBox=\"0 0 250 250\" \r\n style=\"shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd\">\r\n <path fill=\"${buttonOpenedColor}\" d=\"M46.62 0l156.76 0c25.64,0 46.62,20.98 46.62,46.62l0 156.75c0,25.65 -20.98,46.63 -46.62,46.63l-156.76 0c-25.64,0 -46.62,-20.98 -46.62,-46.63l0 -156.75c0,-25.64 20.98,-46.62 46.62,-46.62zm45.71 70.24l32.67 32.67 32.67 -32.67c2.73,-2.73 7.18,-2.73 9.91,0l12.18 12.18c2.73,2.73 2.73,7.18 0,9.91l-32.67 32.67 32.67 32.66c2.73,2.74 2.73,7.19 0,9.92l-12.18 12.18c-2.73,2.73 -7.18,2.73 -9.91,0l-32.67 -32.67 -32.67 32.67c-2.73,2.73 -7.18,2.73 -9.91,0l-12.18 -12.18c-2.73,-2.73 -2.73,-7.18 0,-9.92l32.67 -32.66 -32.67 -32.67c-2.73,-2.73 -2.73,-7.18 0,-9.91l12.18 -12.18c2.73,-2.73 7.18,-2.73 9.91,0z\"/>\r\n </svg>\r\n`;\r\n\r\nconst openSVG = `\r\n <svg xmlns=\"${svgUrl}\" \r\n width=\"${iconSize / 1.6}\" \r\n height=\"${iconSize / 1.6}\" \r\n viewBox=\"0 0 250 250\" \r\n style=\"shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd\">\r\n <path fill=\"${buttonClosedColor}\" d=\"M46.62 0l156.76 0c25.64,0 46.62,20.98 46.62,46.62l0 156.75c0,25.65 -20.98,46.63 -46.62,46.63l-156.76 0c-25.64,0 -46.62,-20.98 -46.62,-46.63l0 -156.75c0,-25.64 20.98,-46.62 46.62,-46.62zm15.5 135.79l57.92 -57.93c2.73,-2.73 7.19,-2.72 9.92,0.01l57.92 57.92c2.73,2.73 2.73,7.18 0,9.91l-12.18 12.18c-2.73,2.73 -7.18,2.73 -9.92,0l-35.82 -35.83c-2.73,-2.73 -7.19,-2.73 -9.92,0l-35.82 35.83c-2.74,2.73 -7.19,2.73 -9.92,0l-12.18 -12.18c-2.73,-2.73 -2.73,-7.18 0,-9.91z\"/>\r\n </svg>\r\n`;\r\n\r\nconst collapseSVG = `\r\n <svg xmlns=\"${svgUrl}\" \r\n width=\"${iconSize / 1.6}\" \r\n height=\"${iconSize / 1.6}\" \r\n viewBox=\"0 0 250 250\"\r\n style=\"shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd\">\r\n <path fill=\"${buttonOpenedColor}\" d=\"M46.62 0l156.76 0c25.64,0 46.62,20.98 46.62,46.62l0 156.75c0,25.65 -20.98,46.63 -46.62,46.63l-156.76 0c-25.64,0 -46.62,-20.98 -46.62,-46.63l0 -156.75c0,-25.64 20.98,-46.62 46.62,-46.62zm109.99 181.69l-75.07 0c-7.3,0 -13.23,-5.92 -13.23,-13.22l0 -75.08c0,-2.35 1.92,-4.28 4.28,-4.28l17.9 0c2.35,0 4.27,1.93 4.27,4.28l0 32.81c0,1.77 1.01,3.28 2.64,3.96 1.63,0.67 3.42,0.33 4.66,-0.93l59.68 -59.68c1.67,-1.65 4.38,-1.65 6.05,0l12.66 12.66c1.66,1.67 1.66,4.38 0,6.05l-59.68 59.68c-1.26,1.24 -1.6,3.03 -0.93,4.66 0.68,1.63 2.19,2.64 3.96,2.64l32.81 0c2.35,0 4.28,1.92 4.28,4.28l0 17.9c0,2.36 -1.93,4.28 -4.28,4.28z\"/>\r\n </svg>\r\n`;\r\n\r\nconst expandSVG = `\r\n <svg xmlns=\"${svgUrl}\" \r\n width=\"${iconSize / 1.6}\" \r\n height=\"${iconSize / 1.6}\" \r\n viewBox=\"0 0 250 250\"\r\n style=\"shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd\">\r\n <path fill=\"${buttonClosedColor}\" d=\"M46.62 0l156.76 0c25.64,0 46.62,20.98 46.62,46.62l0 156.75c0,25.65 -20.98,46.63 -46.62,46.63l-156.76 0c-25.64,0 -46.62,-20.98 -46.62,-46.63l0 -156.75c0,-25.64 20.98,-46.62 46.62,-46.62zm46.77 68.31l75.07 0c7.3,0 13.23,5.92 13.23,13.22l0 75.08c0,2.35 -1.92,4.28 -4.28,4.28l-17.9 0c-2.35,0 -4.27,-1.93 -4.27,-4.28l0 -32.81c0,-1.77 -1.01,-3.28 -2.64,-3.96 -1.63,-0.67 -3.42,-0.33 -4.66,0.93l-59.68 59.68c-1.67,1.65 -4.38,1.65 -6.05,0l-12.66 -12.66c-1.66,-1.67 -1.66,-4.38 0,-6.05l59.68 -59.68c1.26,-1.24 1.6,-3.03 0.93,-4.66 -0.68,-1.63 -2.19,-2.64 -3.96,-2.64l-32.81 0c-2.35,0 -4.28,-1.92 -4.28,-4.27l0 -17.9c0,-2.36 1.93,-4.28 4.28,-4.28z\"/>\r\n </svg>\r\n`;\r\n\r\nconst helpSVG = `\r\n <svg xmlns=\"${svgUrl}\" \r\n width=\"${iconSize / 1.6}\" \r\n height=\"${iconSize / 1.6}\" \r\n viewBox=\"0 0 250 250\"\r\n style=\"shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd\">\r\n <path fill=\"${buttonOpenedColor}\" d=\"M46.59 0l156.82 0c25.65,0 46.59,20.94 46.59,46.59l0 156.82c0,25.65 -20.94,46.59 -46.59,46.59l-156.82 0c-25.65,0 -46.59,-20.94 -46.59,-46.59l0 -156.82c0,-25.65 20.94,-46.59 46.59,-46.59zm130.96 87.85c0,6.57 -1.03,12.72 -3.08,18.06 -2.05,5.34 -4.93,9.85 -8.42,13.75 -3.69,3.9 -8,7.39 -13.13,10.47 -4.11,2.46 -8.83,4.93 -13.96,6.98 -1.64,0.82 -2.87,2.46 -2.87,4.51l0 15.19c0,2.67 -2.06,4.72 -4.73,4.72l-25.04 0c-2.66,0 -4.92,-2.05 -4.92,-4.72l0 -25.65c0,-2.26 1.44,-3.9 3.49,-4.52 2.87,-1.02 5.95,-2.05 9.23,-3.28 4.52,-1.85 8.62,-3.9 12.11,-6.57 3.9,-2.67 6.78,-5.75 9.24,-9.24 2.46,-3.49 3.49,-7.59 3.49,-12.31 0,-6.78 -2.05,-11.7 -6.36,-14.78 -4.31,-2.88 -10.06,-4.52 -17.65,-4.52 -5.75,0 -11.7,1.24 -18.07,3.7 -5.33,2.05 -9.85,4.31 -13.13,6.36 -1.03,0.62 -2.26,0.62 -3.29,0 -1.02,-0.62 -1.64,-1.64 -1.64,-2.87l0 -23.2c0,-2.05 1.23,-3.9 3.08,-4.51 4.1,-1.44 9.44,-3.08 15.8,-4.52 8.42,-2.05 17.24,-3.07 26.89,-3.07 8.42,0 16.01,1.02 22.58,3.07 6.36,2.06 12.11,4.72 16.62,8.42 4.52,3.49 7.8,7.8 10.27,12.72 2.26,4.73 3.49,10.06 3.49,15.81l0 0zm-46.19 114.12l-25.04 0c-2.67,0 -4.92,-2.05 -4.92,-4.72l0 -17.24c0,-2.67 2.25,-4.72 4.92,-4.72l25.04 0c2.67,0 4.72,2.05 4.72,4.72l0 17.24c0,2.67 -2.05,4.72 -4.72,4.72z\"/>\r\n </svg>\r\n`;\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/icons.js?");
/***/ }),
/***/ "./src/messageManager.js":
/*!*******************************!*\
!*** ./src/messageManager.js ***!
\*******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ MessageManager)\n/* harmony export */ });\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./helpers.js */ \"./src/helpers.js\");\n\r\n\r\nclass MessageManager {\r\n constructor(panelId = 'messages-panel', currentUsername = '') {\r\n this.panel = document.getElementById(panelId);\r\n this.messages = [];\r\n this.messageIdCounter = 0;\r\n this.currentUsername = currentUsername;\r\n this.sentMessageTexts = new Set(); // Track recently sent messages\r\n this.processedMessageIds = new Set(); // Now used exclusively for deduplication\r\n this.chatHistory = new Map(); // Local in-memory map for chat history\r\n }\r\n\r\n processMessages(xmlResponse) {\r\n if (!xmlResponse || typeof xmlResponse !== 'string') return;\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(xmlResponse, \"text/xml\");\r\n const messageElements = doc.getElementsByTagName(\"message\");\r\n let newMessagesAdded = false;\r\n\r\n Array.from(messageElements).forEach(msg => {\r\n const bodyNode = msg.getElementsByTagName(\"body\")[0];\r\n if (!bodyNode || !bodyNode.textContent) return;\r\n\r\n const text = bodyNode.textContent.trim();\r\n if (text === \"This room is not anonymous\") return;\r\n\r\n const fromAttr = msg.getAttribute(\"from\");\r\n const from = fromAttr ? fromAttr.split('#')[1]?.split('@')[0] || \"unknown\" : \"unknown\";\r\n const cleanFrom = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.parseUsername)(from);\r\n\r\n // Generate a unique id based solely on username and message text.\r\n const uniqueId = `[${cleanFrom}] ${text}`;\r\n\r\n // Skip this message if it has already been processed.\r\n if (this.processedMessageIds.has(uniqueId)) return;\r\n\r\n // Get timestamp from <delay> if available, otherwise use current time.\r\n let timestamp = new Date().toISOString();\r\n const delayNodes = msg.getElementsByTagName(\"delay\");\r\n if (delayNodes.length && delayNodes[0].getAttribute(\"stamp\")) {\r\n timestamp = delayNodes[0].getAttribute(\"stamp\");\r\n }\r\n\r\n const toAttr = msg.getAttribute(\"to\");\r\n const type = msg.getAttribute(\"type\");\r\n const isPrivate = type === 'chat';\r\n let recipient = null;\r\n if (isPrivate && toAttr) {\r\n recipient = toAttr.split('#')[1]?.split('@')[0] || toAttr;\r\n recipient = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.parseUsername)(recipient);\r\n }\r\n\r\n const messageObj = {\r\n id: uniqueId,\r\n from: cleanFrom,\r\n text,\r\n timestamp,\r\n isPrivate,\r\n recipient,\r\n pending: false\r\n };\r\n\r\n this.messages.push(messageObj);\r\n this.chatHistory.set(uniqueId, messageObj);\r\n this.processedMessageIds.add(uniqueId);\r\n newMessagesAdded = true;\r\n });\r\n\r\n if (newMessagesAdded) {\r\n this.updatePanel();\r\n }\r\n }\r\n\r\n // Method to add a sent message.\r\n addSentMessage(text, options = {}) {\r\n this.sentMessageTexts.add(text);\r\n\r\n // Generate a unique ID using the same format as processMessages\r\n const uniqueId = `[${this.currentUsername}] ${text}`;\r\n\r\n const messageObj = {\r\n id: uniqueId,\r\n from: this.currentUsername,\r\n text,\r\n timestamp: new Date().toISOString(),\r\n isPrivate: options.isPrivate || false,\r\n recipient: options.recipient || null,\r\n pending: options.pending || false\r\n };\r\n\r\n // Skip if this exact message has already been processed\r\n if (this.processedMessageIds.has(uniqueId)) return;\r\n\r\n this.messages.push(messageObj);\r\n this.chatHistory.set(uniqueId, messageObj);\r\n this.processedMessageIds.add(uniqueId);\r\n this.updatePanel();\r\n\r\n // Limit the size of the sent messages set.\r\n if (this.sentMessageTexts.size > 20) {\r\n const entries = Array.from(this.sentMessageTexts);\r\n for (let i = 0; i < entries.length - 20; i++) {\r\n this.sentMessageTexts.delete(entries[i]);\r\n }\r\n }\r\n\r\n return uniqueId; // Return the ID so it can be used for updating pending status\r\n }\r\n\r\n // New: Update pending status of a message by ID.\r\n updatePendingStatus(messageId, pendingStatus) {\r\n const msg = this.chatHistory.get(messageId);\r\n if (msg) {\r\n msg.pending = pendingStatus;\r\n this.updatePanel();\r\n }\r\n }\r\n\r\n updatePanel() {\r\n if (!this.panel) return;\r\n // Ensure messages are in chronological order.\r\n this.messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));\r\n\r\n // Get IDs of messages already rendered.\r\n const renderedIds = new Set(\r\n Array.from(this.panel.querySelectorAll('.message')).map(el => el.getAttribute('data-message-id'))\r\n );\r\n\r\n // Append only messages that haven't been rendered.\r\n this.messages.forEach(msg => {\r\n if (!renderedIds.has(msg.id)) {\r\n const date = new Date(msg.timestamp);\r\n const formattedTime = date.toLocaleTimeString('en-GB', { hour12: false });\r\n const normalizedUsername = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.parseUsername)(msg.from);\r\n const usernameColor = _helpers_js__WEBPACK_IMPORTED_MODULE_0__.usernameColors.getColor(normalizedUsername);\r\n\r\n const messageDiv = document.createElement('div');\r\n messageDiv.className = 'message';\r\n\r\n if (msg.isPrivate) {\r\n messageDiv.classList.add('private-message');\r\n messageDiv.classList.add(msg.from === this.currentUsername ? 'sent' : 'received');\r\n if (msg.recipient) {\r\n messageDiv.setAttribute('data-recipient', msg.recipient);\r\n }\r\n }\r\n\r\n if (msg.text.startsWith('/me ')) {\r\n messageDiv.classList.add('system');\r\n msg.text = `${msg.from} ${msg.text.substring(msg.text.indexOf(' ') + 1)}`;\r\n }\r\n\r\n if (msg.isSystem) {\r\n messageDiv.classList.add('system');\r\n }\r\n\r\n messageDiv.setAttribute('data-message-id', msg.id);\r\n\r\n const messageInfoDiv = document.createElement('div');\r\n messageInfoDiv.className = 'message-info';\r\n\r\n let usernameDisplay = msg.from;\r\n if (msg.isPrivate) {\r\n usernameDisplay = msg.from === this.currentUsername && msg.recipient\r\n ? `→ ${msg.recipient}`\r\n : `${msg.from} →`;\r\n }\r\n\r\n messageInfoDiv.innerHTML = `\r\n <span class=\"time\">${formattedTime}</span>\r\n <span class=\"username\" style=\"color: ${usernameColor}\">${usernameDisplay}</span>\r\n `;\r\n\r\n const messageTextDiv = document.createElement('div');\r\n messageTextDiv.className = 'message-text';\r\n messageTextDiv.innerHTML = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.parseMessageText)(msg.text);\r\n\r\n // Append pending icon if message is pending.\r\n if (msg.pending) {\r\n const pendingIcon = document.createElement('span');\r\n pendingIcon.className = 'pending-emoji';\r\n pendingIcon.textContent = ' ⏱️';\r\n messageTextDiv.appendChild(pendingIcon);\r\n }\r\n\r\n messageDiv.appendChild(messageInfoDiv);\r\n messageDiv.appendChild(messageTextDiv);\r\n this.panel.appendChild(messageDiv);\r\n }\r\n });\r\n\r\n this.addDelegatedClickListeners();\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.highlightMentionWords)([this.currentUsername]);\r\n\r\n requestAnimationFrame(() => {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.scrollToBottom)();\r\n });\r\n }\r\n\r\n addDelegatedClickListeners() {\r\n // Attach the listener only once using a custom flag.\r\n if (!this.panel._delegatedClickAttached) {\r\n this.panel.addEventListener(\"click\", (event) => {\r\n // --- Username click handling --- \r\n const usernameEl = event.target.closest('.username');\r\n if (usernameEl && this.panel.contains(usernameEl)) {\r\n const usernameText = usernameEl.textContent.trim();\r\n let selectedUsername = usernameText;\r\n // Handle arrow formatting used in private messages.\r\n if (selectedUsername.includes('→')) {\r\n if (selectedUsername.startsWith('→')) {\r\n selectedUsername = selectedUsername.replace('→', '').trim();\r\n } else {\r\n selectedUsername = selectedUsername.split('→')[0].trim();\r\n }\r\n }\r\n const messageInput = document.getElementById('message-input');\r\n if (event.ctrlKey) {\r\n // Ctrl+Click: Prepare for a private message.\r\n messageInput.value = `/pm ${selectedUsername} `;\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.handlePrivateMessageInput)(messageInput);\r\n } else {\r\n // Normal click: Append username if not already present.\r\n const appendUsername = `${selectedUsername}, `;\r\n if (!messageInput.value.includes(appendUsername)) {\r\n messageInput.value += appendUsername;\r\n }\r\n }\r\n messageInput.focus();\r\n }\r\n\r\n // --- Time element click handling --- \r\n const timeEl = event.target.closest('.time');\r\n if (timeEl && this.panel.contains(timeEl)) {\r\n // Extract the local time text (assumed format \"HH:MM:SS\")\r\n const localTime = timeEl.textContent.trim();\r\n // Calibrate the time using the provided function.\r\n const moscowTime = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.calibrateToMoscowTime)(localTime);\r\n // Create today's date in the format \"YYYY-MM-DD\"\r\n const today = new Intl.DateTimeFormat('en-CA').format(new Date());\r\n // Build the URL to the chat logs with the calibrated time as the hash.\r\n const url = `https://klavogonki.ru/chatlogs/${today}.html#${moscowTime}`;\r\n\r\n // Open in a new tab.\r\n window.open(url, '_blank');\r\n }\r\n });\r\n // Use a unified flag to ensure the listener is attached only once.\r\n this.panel._delegatedClickAttached = true;\r\n }\r\n }\r\n\r\n getChatHistory() {\r\n return Array.from(this.chatHistory.values());\r\n }\r\n\r\n clearMessages() {\r\n const systemMessageId = `system_${Date.now()}`;\r\n const systemMessage = {\r\n id: systemMessageId,\r\n from: \"System\",\r\n text: \"Chat connection lost. Reconnecting...\",\r\n timestamp: new Date().toISOString(),\r\n isPrivate: false,\r\n recipient: null,\r\n isSystem: true,\r\n pending: false\r\n };\r\n\r\n this.messages.push(systemMessage);\r\n this.chatHistory.set(systemMessageId, systemMessage);\r\n this.processedMessageIds.add(systemMessageId);\r\n this.updatePanel();\r\n }\r\n}\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/messageManager.js?");
/***/ }),
/***/ "./src/styles/emojiPanel.css":
/*!***********************************!*\
!*** ./src/styles/emojiPanel.css ***!
\***********************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ \"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/styleDomAPI.js */ \"./node_modules/style-loader/dist/runtime/styleDomAPI.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/insertBySelector.js */ \"./node_modules/style-loader/dist/runtime/insertBySelector.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js */ \"./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/insertStyleElement.js */ \"./node_modules/style-loader/dist/runtime/insertStyleElement.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/styleTagTransform.js */ \"./node_modules/style-loader/dist/runtime/styleTagTransform.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _node_modules_css_loader_dist_cjs_js_emojiPanel_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! !!../../node_modules/css-loader/dist/cjs.js!./emojiPanel.css */ \"./node_modules/css-loader/dist/cjs.js!./src/styles/emojiPanel.css\");\n\n \n \n \n \n \n \n \n \n \n\nvar options = {};\n\noptions.styleTagTransform = (_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default());\noptions.setAttributes = (_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default());\n\n options.insert = _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default().bind(null, \"head\");\n \noptions.domAPI = (_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default());\noptions.insertStyleElement = (_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default());\n\nvar update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_emojiPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"], options);\n\n\n\n\n /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_css_loader_dist_cjs_js_emojiPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"] && _node_modules_css_loader_dist_cjs_js_emojiPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals ? _node_modules_css_loader_dist_cjs_js_emojiPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals : undefined);\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/styles/emojiPanel.css?");
/***/ }),
/***/ "./src/styles/helpPanel.css":
/*!**********************************!*\
!*** ./src/styles/helpPanel.css ***!
\**********************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ \"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/styleDomAPI.js */ \"./node_modules/style-loader/dist/runtime/styleDomAPI.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/insertBySelector.js */ \"./node_modules/style-loader/dist/runtime/insertBySelector.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js */ \"./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/insertStyleElement.js */ \"./node_modules/style-loader/dist/runtime/insertStyleElement.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/styleTagTransform.js */ \"./node_modules/style-loader/dist/runtime/styleTagTransform.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _node_modules_css_loader_dist_cjs_js_helpPanel_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! !!../../node_modules/css-loader/dist/cjs.js!./helpPanel.css */ \"./node_modules/css-loader/dist/cjs.js!./src/styles/helpPanel.css\");\n\n \n \n \n \n \n \n \n \n \n\nvar options = {};\n\noptions.styleTagTransform = (_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default());\noptions.setAttributes = (_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default());\n\n options.insert = _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default().bind(null, \"head\");\n \noptions.domAPI = (_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default());\noptions.insertStyleElement = (_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default());\n\nvar update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_helpPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"], options);\n\n\n\n\n /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_css_loader_dist_cjs_js_helpPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"] && _node_modules_css_loader_dist_cjs_js_helpPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals ? _node_modules_css_loader_dist_cjs_js_helpPanel_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals : undefined);\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/styles/helpPanel.css?");
/***/ }),
/***/ "./src/styles/style.css":
/*!******************************!*\
!*** ./src/styles/style.css ***!
\******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ \"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/styleDomAPI.js */ \"./node_modules/style-loader/dist/runtime/styleDomAPI.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/insertBySelector.js */ \"./node_modules/style-loader/dist/runtime/insertBySelector.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js */ \"./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/insertStyleElement.js */ \"./node_modules/style-loader/dist/runtime/insertStyleElement.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! !../../node_modules/style-loader/dist/runtime/styleTagTransform.js */ \"./node_modules/style-loader/dist/runtime/styleTagTransform.js\");\n/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! !!../../node_modules/css-loader/dist/cjs.js!./style.css */ \"./node_modules/css-loader/dist/cjs.js!./src/styles/style.css\");\n\n \n \n \n \n \n \n \n \n \n\nvar options = {};\n\noptions.styleTagTransform = (_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default());\noptions.setAttributes = (_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default());\n\n options.insert = _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default().bind(null, \"head\");\n \noptions.domAPI = (_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default());\noptions.insertStyleElement = (_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default());\n\nvar update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"], options);\n\n\n\n\n /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"] && _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals ? _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__[\"default\"].locals : undefined);\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/styles/style.css?");
/***/ }),
/***/ "./src/userManager.js":
/*!****************************!*\
!*** ./src/userManager.js ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ UserManager)\n/* harmony export */ });\n/* harmony import */ var _definitions__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./definitions */ \"./src/definitions.js\");\n/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./helpers */ \"./src/helpers.js\");\n/* harmony import */ var _animations__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./animations */ \"./src/animations.js\");\n\r\n\r\n\r\n\r\nclass UserManager {\r\n constructor(containerId = 'user-list') {\r\n this.container = document.getElementById(containerId);\r\n this.activeUsers = new Map();\r\n this.isFirstLoad = true;\r\n this.avatarCache = this.loadAvatarCache();\r\n this.cacheDate = new Date().toDateString();\r\n\r\n // Role-based icons\r\n this.roleIcons = {\r\n 'visitor': '🐥',\r\n 'participant': '🗿',\r\n 'moderator': '⚔️️'\r\n };\r\n\r\n // Role priority for sorting\r\n this.rolePriority = {\r\n 'moderator': 1,\r\n 'participant': 2,\r\n 'visitor': 3\r\n };\r\n\r\n // Attach event listeners\r\n this.setupEventListeners();\r\n }\r\n\r\n loadAvatarCache() {\r\n try {\r\n const cacheData = localStorage.getItem('userAvatarCache');\r\n if (cacheData) {\r\n const cache = JSON.parse(cacheData);\r\n if (cache.date === new Date().toDateString()) {\r\n console.log(\"🗃️ Loaded avatar cache from localStorage\");\r\n return cache.avatars || {};\r\n } else {\r\n console.log(\"🗃️ Avatar cache expired (new day), creating fresh cache\");\r\n return {};\r\n }\r\n }\r\n } catch (error) {\r\n console.error(\"Error loading avatar cache:\", error);\r\n }\r\n return {};\r\n }\r\n\r\n saveAvatarCache() {\r\n try {\r\n localStorage.setItem('userAvatarCache', JSON.stringify({\r\n date: this.cacheDate,\r\n avatars: this.avatarCache\r\n }));\r\n } catch (error) {\r\n console.error(\"Error saving avatar cache:\", error);\r\n }\r\n }\r\n\r\n setupEventListeners() {\r\n this.container.addEventListener('click', (event) => {\r\n // Handle username clicks\r\n if (event.target.classList.contains('username-clickable')) {\r\n const userId = event.target.getAttribute('data-user-id');\r\n if (userId) {\r\n if (event.ctrlKey) {\r\n // Ctrl+Click: Start private chat with user\r\n const username = event.target.textContent.trim();\r\n const messageInput = document.getElementById('message-input');\r\n messageInput.value = `/pm ${username} `;\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.handlePrivateMessageInput)(messageInput);\r\n messageInput.focus();\r\n } else {\r\n // Normal click: Navigate to profile\r\n const userIdWithoutDomain = userId.split('/')[1].split('#')[0];\r\n window.location.href = `https://klavogonki.ru/u/#/${userIdWithoutDomain}/`;\r\n }\r\n }\r\n }\r\n\r\n // Handle game indicator clicks\r\n if (event.target.closest('.game-indicator')) {\r\n const gameIndicator = event.target.closest('.game-indicator');\r\n const gameId = gameIndicator.getAttribute('data-game-id');\r\n if (gameId) {\r\n event.stopPropagation();\r\n window.location.href = `https://klavogonki.ru/g/?gmid=${gameId}`;\r\n }\r\n }\r\n });\r\n }\r\n\r\n async updatePresence(xmlResponse) {\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(xmlResponse, \"text/xml\");\r\n const presences = doc.getElementsByTagName(\"presence\");\r\n\r\n if (xmlResponse.includes('<presence id=\"pres_1\"')) {\r\n console.log(\"🔄 Initial room join detected, requesting full roster\");\r\n this.requestFullRoster();\r\n return;\r\n }\r\n\r\n let changes = false;\r\n const newUserJIDs = [];\r\n const updatedUserJIDs = [];\r\n\r\n for (let i = 0; i < presences.length; i++) {\r\n const presence = presences[i];\r\n const from = presence.getAttribute('from');\r\n const type = presence.getAttribute('type');\r\n\r\n // Skip if not from the conference room\r\n if (!from || !from.includes('[email protected]/')) {\r\n continue;\r\n }\r\n\r\n // Extract username from JID\r\n const usernameFromJid = from.split('/').pop();\r\n if (!usernameFromJid) continue;\r\n\r\n // Skip Клавобот (unified check)\r\n if (usernameFromJid.toLowerCase() === 'клавобот' || from.toLowerCase().includes('#клавобот')) {\r\n continue;\r\n }\r\n\r\n // Handle user leaving\r\n if (type === 'unavailable') {\r\n if (this.activeUsers.has(from)) {\r\n const departingUser = this.activeUsers.get(from);\r\n const userId = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractUserId)(from);\r\n const cleanLogin = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(departingUser.login);\r\n\r\n if (!this.isFirstLoad) {\r\n // console.log(`🚪 User left: ${cleanLogin} ID: (${userId})`);\r\n }\r\n this.activeUsers.delete(from);\r\n changes = true;\r\n }\r\n continue;\r\n }\r\n\r\n const existingUser = this.activeUsers.get(from) || {};\r\n const userId = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractUserId)(from);\r\n const cachedAvatarInfo = this.avatarCache[userId];\r\n\r\n // Initialize userData\r\n let userData = {\r\n jid: from,\r\n login: usernameFromJid,\r\n color: '#777',\r\n // Normalize the username before calling getColor:\r\n usernameColor: _helpers__WEBPACK_IMPORTED_MODULE_1__.usernameColors.getColor((0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(usernameFromJid)),\r\n role: 'participant',\r\n gameId: null,\r\n avatar: null\r\n };\r\n\r\n // Process x elements\r\n const xElements = presence.getElementsByTagName(\"x\");\r\n let foundAvatar = false;\r\n\r\n for (let j = 0; j < xElements.length; j++) {\r\n const xmlns = xElements[j].getAttribute(\"xmlns\");\r\n if (xmlns === \"klavogonki:userdata\") {\r\n const userNode = xElements[j].getElementsByTagName(\"user\")[0];\r\n if (userNode) {\r\n const loginElement = userNode.getElementsByTagName(\"login\")[0];\r\n // Later, when the login element is updated:\r\n if (loginElement && loginElement.textContent) {\r\n userData.login = loginElement.textContent;\r\n // Again, normalize the username for color generation:\r\n userData.usernameColor = _helpers__WEBPACK_IMPORTED_MODULE_1__.usernameColors.getColor((0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(userData.login));\r\n }\r\n\r\n const avatarElement = userNode.getElementsByTagName(\"avatar\")[0];\r\n if (avatarElement && avatarElement.textContent) {\r\n userData.avatar = avatarElement.textContent;\r\n foundAvatar = true;\r\n }\r\n\r\n const moderatorNode = userNode.getElementsByTagName(\"moderator\")[0];\r\n if (moderatorNode && moderatorNode.textContent === '1') {\r\n userData.role = 'moderator';\r\n }\r\n }\r\n\r\n const gameIdElement = xElements[j].getElementsByTagName(\"game_id\")[0];\r\n if (gameIdElement && gameIdElement.textContent) {\r\n userData.gameId = gameIdElement.textContent;\r\n }\r\n }\r\n\r\n if (xmlns === \"http://jabber.org/protocol/muc#user\") {\r\n const itemNode = xElements[j].getElementsByTagName(\"item\")[0];\r\n if (itemNode) {\r\n const role = itemNode.getAttribute(\"role\");\r\n if (role && userData.role !== 'moderator') {\r\n userData.role = role;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // If no avatar in update but exists in user data, keep it\r\n if (!foundAvatar && existingUser && existingUser.avatar) {\r\n userData.avatar = existingUser.avatar;\r\n }\r\n\r\n // Handle avatar (use cache or set default)\r\n if (!userData.avatar && cachedAvatarInfo) {\r\n if (cachedAvatarInfo.hasAvatar) {\r\n userData.avatar = cachedAvatarInfo.avatarUrl;\r\n }\r\n }\r\n\r\n const cleanLogin = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(userData.login);\r\n\r\n // Determine if user is new or updated\r\n if (!this.activeUsers.has(from)) {\r\n if (!this.isFirstLoad) {\r\n // console.log(`👤 User joined: ${cleanLogin} ID: (${userId})`);\r\n }\r\n this.activeUsers.set(from, userData);\r\n changes = true;\r\n newUserJIDs.push(from);\r\n } else if (JSON.stringify(existingUser) !== JSON.stringify(userData)) {\r\n this.activeUsers.set(from, userData);\r\n changes = true;\r\n updatedUserJIDs.push(from);\r\n }\r\n }\r\n\r\n if (changes) {\r\n this.updateUI(newUserJIDs, updatedUserJIDs);\r\n }\r\n }\r\n\r\n updateUI(newUserJIDs = [], updatedUserJIDs = []) {\r\n // Build map of existing DOM elements\r\n const existingElements = new Map();\r\n this.container.querySelectorAll('.user-item').forEach(el => {\r\n existingElements.set(el.getAttribute('data-jid'), el);\r\n });\r\n\r\n // Sort users by role and username\r\n const sortedUsers = Array.from(this.activeUsers.values()).sort((a, b) => {\r\n const priorityDiff = this.rolePriority[a.role] - this.rolePriority[b.role];\r\n return priorityDiff !== 0 ? priorityDiff :\r\n (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(a.login).localeCompare((0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(b.login));\r\n });\r\n\r\n // Build the updated list\r\n const fragment = document.createDocumentFragment();\r\n sortedUsers.forEach(user => {\r\n let userElement = existingElements.get(user.jid);\r\n const userId = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractUserId)(user.jid);\r\n const cleanLogin = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.extractCleanUsername)(user.login);\r\n\r\n // If element doesn't exist, create it\r\n if (!userElement) {\r\n userElement = document.createElement('div');\r\n userElement.classList.add('user-item');\r\n userElement.setAttribute('data-jid', user.jid);\r\n const roleIcon = this.roleIcons[user.role] || '👤';\r\n\r\n const avatarContainer = document.createElement('span');\r\n avatarContainer.className = 'avatar-container';\r\n this.setUserAvatar(avatarContainer, user, userId, cleanLogin);\r\n\r\n // Create user info container\r\n const userInfo = document.createElement('div');\r\n userInfo.className = 'user-info';\r\n userInfo.innerHTML = `\r\n <div class=\"username\" style=\"color: ${user.usernameColor}\">\r\n <span class=\"username-clickable\" data-user-id=\"${user.jid}\">${cleanLogin}</span>\r\n <span class=\"role ${user.role}\">${roleIcon}</span>\r\n </div>\r\n `;\r\n\r\n // Append avatar and user info\r\n userElement.appendChild(avatarContainer);\r\n userElement.appendChild(userInfo);\r\n } else {\r\n // Update existing element if needed\r\n if (!userElement.querySelector('.avatar-container')) {\r\n const avatarContainer = document.createElement('span');\r\n avatarContainer.className = 'avatar-container';\r\n this.setUserAvatar(avatarContainer, user, userId, cleanLogin);\r\n userElement.insertBefore(avatarContainer, userElement.firstChild);\r\n }\r\n\r\n // Remove from map so remaining elements are those to be removed\r\n existingElements.delete(user.jid);\r\n\r\n // Update role icon if changed\r\n const roleElement = userElement.querySelector('.role');\r\n const newRoleIcon = this.roleIcons[user.role] || '👤';\r\n if (roleElement && roleElement.textContent !== newRoleIcon) {\r\n roleElement.textContent = newRoleIcon;\r\n if (!roleElement.classList.contains(user.role)) {\r\n roleElement.className = `role ${user.role}`;\r\n }\r\n }\r\n\r\n // Update username color if needed\r\n const usernameElement = userElement.querySelector('.username');\r\n if (usernameElement && usernameElement.style.color !== user.usernameColor) {\r\n usernameElement.style.color = user.usernameColor;\r\n }\r\n }\r\n\r\n // Handle game indicator\r\n this.updateGameIndicator(userElement, user);\r\n\r\n // Append to fragment\r\n fragment.appendChild(userElement);\r\n });\r\n\r\n // Clear container and append fragment\r\n this.container.innerHTML = '';\r\n this.container.appendChild(fragment);\r\n\r\n // Remove elements for users no longer active\r\n existingElements.forEach((el) => {\r\n if (el && el.parentNode) {\r\n el.remove();\r\n }\r\n });\r\n\r\n // Apply shake effect for new users\r\n if (!this.isFirstLoad) {\r\n newUserJIDs.forEach(jid => {\r\n const userElement = this.container.querySelector(`.user-item[data-jid=\"${jid}\"]`);\r\n if (userElement && userElement.parentNode) {\r\n (0,_animations__WEBPACK_IMPORTED_MODULE_2__.addShakeEffect)(userElement);\r\n }\r\n });\r\n }\r\n\r\n if (this.isFirstLoad) {\r\n this.isFirstLoad = false;\r\n }\r\n }\r\n\r\n setUserAvatar(avatarContainer, user, userId, cleanLogin) {\r\n const cachedAvatarInfo = this.avatarCache[userId];\r\n\r\n // Display avatar based on available information\r\n if (user.avatar) {\r\n const avatarUrl = `${_definitions__WEBPACK_IMPORTED_MODULE_0__.BASE_URL}/storage/avatars/${userId}_big.png`;\r\n const avatarImg = document.createElement('img');\r\n avatarImg.className = 'user-avatar image-avatar';\r\n avatarImg.src = avatarUrl;\r\n avatarImg.alt = `${cleanLogin}'s avatar`;\r\n\r\n // Handle error by replacing with emoji\r\n avatarImg.addEventListener('error', () => {\r\n const fallbackEmoji = cachedAvatarInfo?.emoji || (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.getRandomEmojiAvatar)();\r\n avatarContainer.innerHTML = '';\r\n const fallbackSpan = document.createElement('span');\r\n fallbackSpan.className = 'user-avatar svg-avatar';\r\n fallbackSpan.textContent = fallbackEmoji;\r\n avatarContainer.appendChild(fallbackSpan);\r\n\r\n // Update cache\r\n this.avatarCache[userId] = {\r\n hasAvatar: false,\r\n emoji: fallbackEmoji\r\n };\r\n this.saveAvatarCache();\r\n });\r\n\r\n // On successful load, update cache\r\n avatarImg.addEventListener('load', () => {\r\n // Only log first time we detect an avatar\r\n if (!cachedAvatarInfo || !cachedAvatarInfo.hasAvatar) {\r\n // console.log(`🖼️ Using image avatar for User: ${cleanLogin} ID: (${userId})`);\r\n }\r\n this.avatarCache[userId] = {\r\n hasAvatar: true,\r\n avatarUrl: avatarUrl\r\n };\r\n this.saveAvatarCache();\r\n });\r\n\r\n avatarContainer.appendChild(avatarImg);\r\n\r\n } else if (cachedAvatarInfo) {\r\n // Use cached information\r\n if (cachedAvatarInfo.hasAvatar) {\r\n const avatarImg = document.createElement('img');\r\n avatarImg.className = 'user-avatar image-avatar';\r\n avatarImg.src = cachedAvatarInfo.avatarUrl;\r\n avatarImg.alt = `${cleanLogin}'s avatar`;\r\n\r\n // Handle error if cache is incorrect\r\n avatarImg.addEventListener('error', () => {\r\n const fallbackEmoji = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.getRandomEmojiAvatar)();\r\n avatarContainer.innerHTML = '';\r\n const fallbackSpan = document.createElement('span');\r\n fallbackSpan.className = 'user-avatar svg-avatar';\r\n fallbackSpan.textContent = fallbackEmoji;\r\n avatarContainer.appendChild(fallbackSpan);\r\n\r\n // Update cache\r\n this.avatarCache[userId] = {\r\n hasAvatar: false,\r\n emoji: fallbackEmoji\r\n };\r\n this.saveAvatarCache();\r\n });\r\n\r\n avatarContainer.appendChild(avatarImg);\r\n } else {\r\n // Use cached emoji\r\n const fallbackSpan = document.createElement('span');\r\n fallbackSpan.className = 'user-avatar svg-avatar';\r\n fallbackSpan.textContent = cachedAvatarInfo.emoji;\r\n avatarContainer.appendChild(fallbackSpan);\r\n\r\n // Only log first time we use an emoji avatar\r\n if (!this.avatarCache[userId] || !this.avatarCache[userId].hasEmoji) {\r\n // console.log(`😊 Using emoji avatar for User: ${cleanLogin} ID: (${userId}): ${cachedAvatarInfo.emoji}`);\r\n // Mark that we've logged this emoji usage\r\n this.avatarCache[userId].hasEmoji = true;\r\n this.saveAvatarCache();\r\n }\r\n }\r\n } else {\r\n // No cached info - try to fetch avatar\r\n const avatarUrl = `${_definitions__WEBPACK_IMPORTED_MODULE_0__.BASE_URL}/storage/avatars/${userId}_big.png`;\r\n const avatarImg = document.createElement('img');\r\n avatarImg.className = 'user-avatar image-avatar';\r\n avatarImg.src = avatarUrl;\r\n avatarImg.alt = `${cleanLogin}'s avatar`;\r\n\r\n // Handle error\r\n avatarImg.addEventListener('error', () => {\r\n const fallbackEmoji = (0,_helpers__WEBPACK_IMPORTED_MODULE_1__.getRandomEmojiAvatar)();\r\n avatarContainer.innerHTML = '';\r\n const fallbackSpan = document.createElement('span');\r\n fallbackSpan.className = 'user-avatar svg-avatar';\r\n fallbackSpan.textContent = fallbackEmoji;\r\n avatarContainer.appendChild(fallbackSpan);\r\n\r\n // Log first time using emoji\r\n console.log(`😊 Using emoji avatar for User: ${cleanLogin} ID: (${userId}): ${fallbackEmoji}`);\r\n\r\n // Cache negative result\r\n this.avatarCache[userId] = {\r\n hasAvatar: false,\r\n emoji: fallbackEmoji,\r\n hasEmoji: true\r\n };\r\n this.saveAvatarCache();\r\n });\r\n\r\n // On successful load, cache positive result\r\n avatarImg.addEventListener('load', () => {\r\n console.log(`🖼️ Using image avatar for User: ${cleanLogin} ID: (${userId})`);\r\n this.avatarCache[userId] = {\r\n hasAvatar: true,\r\n avatarUrl: avatarUrl\r\n };\r\n this.saveAvatarCache();\r\n });\r\n\r\n avatarContainer.appendChild(avatarImg);\r\n }\r\n }\r\n\r\n updateGameIndicator(userElement, user) {\r\n let gameIndicatorElement = userElement.querySelector('.game-indicator');\r\n\r\n if (user.gameId) {\r\n if (!gameIndicatorElement || gameIndicatorElement.getAttribute('data-game-id') !== user.gameId) {\r\n const newIndicatorHTML = `<span class=\"game-indicator\" title=\"${user.gameId}\" data-game-id=\"${user.gameId}\">\r\n <span class=\"traffic-icon\">🚦</span>\r\n </span>`;\r\n if (gameIndicatorElement) {\r\n gameIndicatorElement.outerHTML = newIndicatorHTML;\r\n } else {\r\n const usernameContainer = userElement.querySelector('.username');\r\n usernameContainer.insertAdjacentHTML('beforeend', newIndicatorHTML);\r\n }\r\n }\r\n } else if (gameIndicatorElement && gameIndicatorElement.parentNode) {\r\n gameIndicatorElement.remove();\r\n }\r\n }\r\n\r\n async requestFullRoster() {\r\n console.log(\"📑 Would request full roster here (using existing data for now)\");\r\n this.updateUI();\r\n }\r\n}\n\n//# sourceURL=webpack://tampermonkey-script/./src/userManager.js?");
/***/ }),
/***/ "./src/xmppClient.js":
/*!***************************!*\
!*** ./src/xmppClient.js ***!
\***************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ createXMPPClient: () => (/* binding */ createXMPPClient)\n/* harmony export */ });\n/* harmony import */ var _definitions_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./definitions.js */ \"./src/definitions.js\");\n/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./helpers.js */ \"./src/helpers.js\");\n\r\n\r\n\r\nfunction createXMPPClient(xmppConnection, userManager, messageManager, username) {\r\n // Compact wrapper functions.\r\n const safeUpdatePresence = (xmlResponse) =>\r\n xmlResponse && userManager.updatePresence(xmlResponse);\r\n\r\n const safeProcessMessages = (xmlResponse) =>\r\n xmlResponse && messageManager.processMessages(xmlResponse);\r\n\r\n const xmppClient = {\r\n userManager,\r\n messageManager,\r\n presenceInterval: null,\r\n isReconnecting: false,\r\n isConnected: false,\r\n // Queue of messages waiting to be sent.\r\n messageQueue: [],\r\n\r\n // Helper: Create the XML stanza for a message.\r\n _createMessageStanza(text, messageId, isPrivate, fullJid) {\r\n const userDataBlock = `\r\n <x xmlns='klavogonki:userdata'>\r\n <user>\r\n <login>${username.replace(/^\\d+#/, '')}</login>\r\n <avatar>/storage/avatars/${username.split('#')[0]}.png</avatar>\r\n <background>#123456</background>\r\n </user>\r\n </x>`;\r\n\r\n if (isPrivate && fullJid) {\r\n return `\r\n <body rid='${xmppConnection.nextRid()}' sid='${xmppConnection.sid}' xmlns='http://jabber.org/protocol/httpbind'>\r\n <message from='${username}@jabber.klavogonki.ru/web'\r\n to='${fullJid}'\r\n type='chat'\r\n id='${messageId}'\r\n xmlns='jabber:client'>\r\n <body>${text}</body>\r\n ${userDataBlock}\r\n </message>\r\n </body>`;\r\n } else {\r\n return `\r\n <body rid='${xmppConnection.nextRid()}' sid='${xmppConnection.sid}' xmlns='http://jabber.org/protocol/httpbind'>\r\n <message to='[email protected]'\r\n type='groupchat'\r\n id='${messageId}'\r\n xmlns='jabber:client'>\r\n <body>${text}</body>\r\n ${userDataBlock}\r\n </message>\r\n </body>`;\r\n }\r\n },\r\n\r\n // Process queued messages sequentially.\r\n async processQueue() {\r\n if (!this.isConnected || this.isReconnecting) return;\r\n\r\n while (this.messageQueue.length > 0) {\r\n const msg = this.messageQueue[0]; // Peek the first message.\r\n const messageStanza = this._createMessageStanza(msg.text, msg.id, msg.isPrivate, msg.fullJid);\r\n try {\r\n await xmppConnection.sendRequestWithRetry(messageStanza);\r\n // On success, remove the message from the queue.\r\n this.messageQueue.shift();\r\n // Optionally, update the UI to remove the pending flag.\r\n messageManager.updatePendingStatus(msg.id, false);\r\n safeProcessMessages(null);\r\n } catch (error) {\r\n console.error(`Failed to send queued message (${msg.id}): ${error.message}`);\r\n // Stop processing; the message remains in the queue for later retry.\r\n break;\r\n }\r\n }\r\n },\r\n\r\n async connect() {\r\n try {\r\n if (this.presenceInterval) {\r\n clearInterval(this.presenceInterval);\r\n this.presenceInterval = null;\r\n }\r\n let retries = 5;\r\n while (retries > 0 && !this.isConnected) {\r\n try {\r\n const session = await xmppConnection.connect();\r\n console.log('💬 Step 8: Joining chat room...');\r\n const joinPayload = `<body rid='${xmppConnection.nextRid()}' sid='${session.sid}'\r\n xmlns='http://jabber.org/protocol/httpbind'>\r\n <presence id='pres_1' xmlns='jabber:client' to='[email protected]/${username}'>\r\n <x xmlns='http://jabber.org/protocol/muc'/>\r\n </presence>\r\n </body>`;\r\n const joinResponse = await xmppConnection.sendRequestWithRetry(joinPayload);\r\n console.log('📥 Join response:', joinResponse);\r\n\r\n safeUpdatePresence(joinResponse);\r\n safeProcessMessages(joinResponse);\r\n\r\n const infoPayload = `<body rid='${xmppConnection.nextRid()}' sid='${session.sid}'\r\n xmlns='http://jabber.org/protocol/httpbind'>\r\n <iq type='get' id='info1' xmlns='jabber:client' to='[email protected]'>\r\n <query xmlns='http://jabber.org/protocol/disco#info'/>\r\n </iq>\r\n </body>`;\r\n await xmppConnection.sendRequestWithRetry(infoPayload);\r\n console.log('🚀 Step 10: Connected! Starting presence updates...');\r\n\r\n this.isConnected = true;\r\n if (this.isReconnecting) {\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.showChatAlert)(\"Chat connected successfully!\", { type: 'success' });\r\n this.isReconnecting = false;\r\n }\r\n this.startPresencePolling(xmppConnection);\r\n // Process any queued messages.\r\n this.processQueue();\r\n break;\r\n } catch (error) {\r\n console.error(`💥 Connection error: ${error.message}`);\r\n retries--;\r\n if (retries === 0) {\r\n console.log('⏳ Scheduling reconnection attempt in 5 seconds...');\r\n this.isReconnecting = true;\r\n setTimeout(() => this.connect(), _definitions_js__WEBPACK_IMPORTED_MODULE_0__.reconnectionDelay);\r\n } else {\r\n console.log(`🔄 Retrying connection... (${retries} attempts left)`);\r\n await new Promise(resolve => setTimeout(resolve, 5000));\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`💥 Final connection error: ${error.message}`);\r\n this.isConnected = false;\r\n if (!this.isReconnecting) {\r\n console.log('⏳ Scheduling reconnection attempt in 5 seconds...');\r\n this.isReconnecting = true;\r\n setTimeout(() => this.connect(), _definitions_js__WEBPACK_IMPORTED_MODULE_0__.reconnectionDelay);\r\n }\r\n }\r\n },\r\n\r\n startPresencePolling(xmppConnection) {\r\n this.presenceInterval = setInterval(async () => {\r\n if (!this.isConnected) {\r\n console.log('⚠️ Skipping presence poll - not connected');\r\n return;\r\n }\r\n try {\r\n const xmlResponse = await xmppConnection.sendRequestWithRetry(\r\n `<body rid='${xmppConnection.nextRid()}' sid='${xmppConnection.sid}' xmlns='http://jabber.org/protocol/httpbind'/>`\r\n );\r\n safeUpdatePresence(xmlResponse);\r\n safeProcessMessages(xmlResponse);\r\n this.isReconnecting = false;\r\n } catch (error) {\r\n console.error('Presence polling error:', error.message);\r\n if (error.message.includes('404') && !this.isReconnecting) {\r\n console.log('🛑 Connection lost (404). Reconnecting in 5 seconds...');\r\n (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.showChatAlert)(\"Chat connection lost. Reconnecting...\", { type: 'warning' });\r\n messageManager.clearMessages();\r\n this.isReconnecting = true;\r\n this.isConnected = false;\r\n clearInterval(this.presenceInterval);\r\n this.presenceInterval = null;\r\n setTimeout(() => this.connect(), _definitions_js__WEBPACK_IMPORTED_MODULE_0__.reconnectionDelay);\r\n }\r\n }\r\n }, _definitions_js__WEBPACK_IMPORTED_MODULE_0__.userListDelay);\r\n },\r\n\r\n sendMessage(text) {\r\n const messageId = `msg_${Date.now()}`;\r\n let isPrivate = false;\r\n let fullJid = null;\r\n let recipient = null;\r\n\r\n // Only mark as pending if we're disconnected\r\n const isPending = !this.isConnected || this.isReconnecting;\r\n\r\n // Use original condition for private messages.\r\n if (_helpers_js__WEBPACK_IMPORTED_MODULE_1__.privateMessageState.isPrivateMode && _helpers_js__WEBPACK_IMPORTED_MODULE_1__.privateMessageState.fullJid) {\r\n isPrivate = true;\r\n fullJid = _helpers_js__WEBPACK_IMPORTED_MODULE_1__.privateMessageState.fullJid;\r\n recipient = _helpers_js__WEBPACK_IMPORTED_MODULE_1__.privateMessageState.targetUsername;\r\n // Only mark as pending if we're disconnected\r\n messageManager.addSentMessage(text, {\r\n isPrivate: true,\r\n recipient,\r\n pending: isPending\r\n });\r\n } else {\r\n messageManager.addSentMessage(text, {\r\n pending: isPending\r\n });\r\n }\r\n\r\n // Enqueue the message\r\n this.messageQueue.push({\r\n text,\r\n id: messageId,\r\n isPrivate,\r\n fullJid,\r\n recipient,\r\n pending: isPending\r\n });\r\n\r\n // Process the queue if connected.\r\n if (this.isConnected && !this.isReconnecting) {\r\n this.processQueue();\r\n }\r\n }\r\n };\r\n\r\n return xmppClient;\r\n}\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/xmppClient.js?");
/***/ }),
/***/ "./src/xmppConnection.js":
/*!*******************************!*\
!*** ./src/xmppConnection.js ***!
\*******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ XMPPConnection)\n/* harmony export */ });\nclass XMPPConnection {\r\n constructor({ username, password, bindUrl, delay = 1000 }) {\r\n this.username = username;\r\n this.password = password;\r\n this.bindUrl = bindUrl;\r\n this.delay = delay;\r\n this.sid = null;\r\n this.rid = Math.floor(Date.now() / 1000);\r\n }\r\n\r\n nextRid() {\r\n return ++this.rid;\r\n }\r\n\r\n async sendRequest(payload) {\r\n const response = await fetch(this.bindUrl, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'text/xml; charset=UTF-8',\r\n 'User-Agent': 'Mozilla/5.0'\r\n },\r\n body: payload\r\n });\r\n if (!response.ok) {\r\n throw new Error(`HTTP error! status: ${response.status}`);\r\n }\r\n return await response.text();\r\n }\r\n\r\n async sendRequestWithRetry(payload, maxRetries = 5) {\r\n let lastError;\r\n let baseWaitTime = this.delay;\r\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\r\n try {\r\n return await this.sendRequest(payload);\r\n } catch (error) {\r\n lastError = error;\r\n if (error.message.includes('429')) {\r\n const waitTime = baseWaitTime * Math.pow(2, attempt);\r\n console.log(`⏱️ Rate limited (attempt ${attempt}/${maxRetries}). Waiting ${waitTime}ms...`);\r\n await this.sleep(waitTime);\r\n } else {\r\n throw error;\r\n }\r\n }\r\n }\r\n throw new Error(`Max retries reached. Last error: ${lastError.message}`);\r\n }\r\n\r\n sleep(ms) {\r\n return new Promise(resolve => setTimeout(resolve, ms));\r\n }\r\n\r\n base64Encode(str) {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(str);\r\n return btoa(String.fromCharCode(...data));\r\n }\r\n\r\n async connect() {\r\n console.log('🌐 Step 1: Connecting to XMPP server...');\r\n const initPayload = `<body xmlns='http://jabber.org/protocol/httpbind'\r\n rid='${this.nextRid()}'\r\n to='jabber.klavogonki.ru'\r\n xml:lang='en'\r\n wait='60'\r\n hold='1'\r\n ver='1.6'\r\n xmpp:version='1.0'\r\n xmlns:xmpp='urn:xmpp:xbosh'/>`;\r\n const initResponse = await this.sendRequestWithRetry(initPayload);\r\n this.sid = initResponse.match(/sid=['\"]([^'\"]+)['\"]/)[1];\r\n console.log(`🔑 Step 2: Session ID received: ${this.sid}`);\r\n await this.sleep(this.delay / 8);\r\n console.log('🔐 Step 3: Authenticating...');\r\n const authString = this.base64Encode('\\x00' + this.username + '\\x00' + this.password);\r\n const authPayload = `<body rid='${this.nextRid()}' sid='${this.sid}'\r\n xmlns='http://jabber.org/protocol/httpbind'>\r\n <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>${authString}</auth>\r\n </body>`;\r\n const authResponse = await this.sendRequestWithRetry(authPayload);\r\n if (!authResponse.includes('<success')) {\r\n throw new Error('❌ Authentication failed');\r\n }\r\n console.log('✅ Step 4: Authentication successful!');\r\n await this.sleep(this.delay / 8);\r\n console.log('🔄 Step 5: Restarting stream...');\r\n const restartPayload = `<body rid='${this.nextRid()}' sid='${this.sid}'\r\n xmlns='http://jabber.org/protocol/httpbind'\r\n to='jabber.klavogonki.ru'\r\n xmpp:restart='true'\r\n xmlns:xmpp='urn:xmpp:xbosh'/>`;\r\n await this.sendRequestWithRetry(restartPayload);\r\n await this.sleep(this.delay / 8);\r\n console.log('📦 Step 6: Binding resource...');\r\n const bindPayload = `<body rid='${this.nextRid()}' sid='${this.sid}'\r\n xmlns='http://jabber.org/protocol/httpbind'>\r\n <iq type='set' id='bind_1' xmlns='jabber:client'>\r\n <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>\r\n <resource>web</resource>\r\n </bind>\r\n </iq>\r\n </body>`;\r\n await this.sendRequestWithRetry(bindPayload);\r\n await this.sleep(this.delay / 8);\r\n console.log('🔌 Step 7: Establishing session...');\r\n const sessionPayload = `<body rid='${this.nextRid()}' sid='${this.sid}'\r\n xmlns='http://jabber.org/protocol/httpbind'>\r\n <iq type='set' id='session_1' xmlns='jabber:client'>\r\n <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>\r\n </iq>\r\n </body>`;\r\n await this.sendRequestWithRetry(sessionPayload);\r\n await this.sleep(this.delay / 8);\r\n // Return session details for further use.\r\n return { sid: this.sid, rid: this.rid };\r\n }\r\n}\r\n\n\n//# sourceURL=webpack://tampermonkey-script/./src/xmppConnection.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/nonce */
/******/ (() => {
/******/ __webpack_require__.nc = undefined;
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./main.js");
/******/
/******/ })()
;