diff --git a/Backend/data/gif_categories.json b/Backend/data/gif_categories.json new file mode 100644 index 0000000..cf5a38f --- /dev/null +++ b/Backend/data/gif_categories.json @@ -0,0 +1,240 @@ +{ + "categories": [ + { + "name": "excited", + "src": "https://media.tenor.com/nb9yRj8rHo4AAAPs/excited-ah.webm" + }, + { + "name": "bye", + "src": "https://media.tenor.com/63TUZpfzGHQAAAPs/peach-and-goma.webm" + }, + { + "name": "sorry", + "src": "https://media.tenor.com/MXhgLF7-FwAAAAPs/sorry.webm" + }, + { + "name": "congratulations", + "src": "https://media.tenor.com/Twpbh1jgXD4AAAPs/congratulations-congrats.webm" + }, + { + "name": "sleepy", + "src": "https://media.tenor.com/Clvc7JcH7z4AAAPs/sleepy-bed-time.webm" + }, + { + "name": "hello", + "src": "https://media.tenor.com/Y6Qq56KBIjUAAAPs/hello-cute.webm" + }, + { + "name": "hugs", + "src": "https://media.tenor.com/oZtU0xcJCdMAAAPs/i-love-you-love-you.webm" + }, + { + "name": "ok", + "src": "https://media.tenor.com/UrIakXGExfUAAAPs/mr-bean.webm" + }, + { + "name": "please", + "src": "https://media.tenor.com/heEyHbV8iaUAAAPs/puss-in-boots-shrek.webm" + }, + { + "name": "thank you", + "src": "https://media.tenor.com/L30xqxi-6L4AAAPs/gitapro3-gitagita.webm" + }, + { + "name": "miss you", + "src": "https://media.tenor.com/rzG9YBjxW-0AAAPs/peach-sad.webm" + }, + { + "name": "wink", + "src": "https://media.tenor.com/KfRIf22QpTQAAAPs/wink-eye-wink.webm" + }, + { + "name": "whatever", + "src": "https://media.tenor.com/5AdGWfejMcoAAAPs/whatever-you-say.webm" + }, + { + "name": "hungry", + "src": "https://media.tenor.com/7tTScpb6gt0AAAPs/spearhyunho.webm" + }, + { + "name": "dance", + "src": "https://media.tenor.com/HCyNMWQv868AAAPs/good-night.webm" + }, + { + "name": "annoyed", + "src": "https://media.tenor.com/6DRQNAOEavcAAAPs/cat-annoyed.webm" + }, + { + "name": "omg", + "src": "https://media.tenor.com/FtUKdJxBRH0AAAPs/cat-cat-memes.webm" + }, + { + "name": "crazy", + "src": "https://media.tenor.com/o2yRyjihS1wAAAPs/balthazar-crazy.webm" + }, + { + "name": "shrug", + "src": "https://media.tenor.com/Zc-ZTPzlEHoAAAPs/i-don%27t-know-idk.webm" + }, + { + "name": "smile", + "src": "https://media.tenor.com/NrEEi7ZDo8cAAAPs/as.webm" + }, + { + "name": "awkward", + "src": "https://media.tenor.com/6Ug_C4RjC4kAAAPs/%D9%84%D9%8A%D9%8A%D9%88%D9%88.webm" + }, + { + "name": "ew", + "src": "https://media.tenor.com/oj4p9mRXeoIAAAPs/dol-huh.webm" + }, + { + "name": "angry", + "src": "https://media.tenor.com/RZzU2_IbHDEAAAPs/cat-side-eye.webm" + }, + { + "name": "surprised", + "src": "https://media.tenor.com/CNI1fSM1XSoAAAPs/shocked-surprised.webm" + }, + { + "name": "why", + "src": "https://media.tenor.com/HewFmNRxtT4AAAPs/why-persian-room-cat-guardian.webm" + }, + { + "name": "thumbs up", + "src": "https://media.tenor.com/LpEzkHFtdgUAAAPs/gif-emoji.webm" + }, + { + "name": "wow", + "src": "https://media.tenor.com/VdsC5bF7CMAAAAPs/smu.webm" + }, + { + "name": "ouch", + "src": "https://media.tenor.com/qG2Qj0vvLwMAAAPs/ouch.webm" + }, + { + "name": "oops", + "src": "https://media.tenor.com/izYQUXHzhxoAAAPs/oops.webm" + }, + { + "name": "youre welcome", + "src": "https://media.tenor.com/CXZLJK_6sa0AAAPs/you%27re-welcome.webm" + }, + { + "name": "lazy", + "src": "https://media.tenor.com/Xt7ns1EZUEIAAAPs/panda-animated.webm" + }, + { + "name": "stressed", + "src": "https://media.tenor.com/Yc-62-d3QCAAAAPs/stressed.webm" + }, + { + "name": "embarrassed", + "src": "https://media.tenor.com/EIlaqgHLePUAAAPs/look-away-simpson.webm" + }, + { + "name": "clapping", + "src": "https://media.tenor.com/3yPBPC_dwe8AAAPs/leonardo-dicaprio-clapping.webm" + }, + { + "name": "awesome", + "src": "https://media.tenor.com/WFhElAbsdqcAAAPs/awesome-minions.webm" + }, + { + "name": "jk", + "src": "https://media.tenor.com/Fy0hkZaMgakAAAPs/nah-im-just-kidding-jk.webm" + }, + { + "name": "good luck", + "src": "https://media.tenor.com/VK6Wv4nxX10AAAPs/good-luck.webm" + }, + { + "name": "high five", + "src": "https://media.tenor.com/HozyHCAac-kAAAPs/high-five-patrick-star.webm" + }, + { + "name": "nervous", + "src": "https://media.tenor.com/tDfXlJnVctgAAAPs/sweating-nervous.webm" + }, + { + "name": "duh", + "src": "https://media.tenor.com/DCScm2moJ7EAAAPs/duh-sarcastic.webm" + }, + { + "name": "aww", + "src": "https://media.tenor.com/XwxrDV7VKuEAAAPs/love-languages.webm" + }, + { + "name": "scared", + "src": "https://media.tenor.com/Io0g8LOf8nMAAAPs/dog-awkward.webm" + }, + { + "name": "bored", + "src": "https://media.tenor.com/f4d9ExQT1voAAAPs/h2di-cat-dead.webm" + }, + { + "name": "sigh", + "src": "https://media.tenor.com/ZFc20z8DItkAAAPs/facepalm-really.webm" + }, + { + "name": "kiss", + "src": "https://media.tenor.com/RPq56gVYswUAAAPs/twitter-kiwi.webm" + }, + { + "name": "sad", + "src": "https://media.tenor.com/lV1EF4I83MkAAAPs/bubu-dudu-twitter.webm" + }, + { + "name": "good night", + "src": "https://media.tenor.com/95lulIY57IwAAAPs/sweet-dreams-sleep-good.webm" + }, + { + "name": "good morning", + "src": "https://media.tenor.com/25Y5C_HeRckAAAPs/good-morning.webm" + }, + { + "name": "confused", + "src": "https://media.tenor.com/ASAsHQNVvacAAAPs/midnight-the-cat-stopped-working-midnight-the-cat.webm" + }, + { + "name": "chill out", + "src": "https://media.tenor.com/65BBEN4WDcUAAAPs/chillin-chilling.webm" + }, + { + "name": "love", + "src": "https://media.tenor.com/1nIDXbABxgsAAAPs/gif-gifkk.webm" + }, + { + "name": "happy", + "src": "https://media.tenor.com/gotOLnyvy4YAAAPs/bubu-dancing-dance.webm" + }, + { + "name": "cry", + "src": "https://media.tenor.com/rokauCI9nPYAAAPs/crying-sad.webm" + }, + { + "name": "yes", + "src": "https://media.tenor.com/7iq8qyXvKHsAAAPs/yes-monkey.webm" + }, + { + "name": "no", + "src": "https://media.tenor.com/vLK4Mq3jiKIAAAPs/cat-no.webm" + }, + { + "name": "lol", + "src": "https://media.tenor.com/JgCg0MPmMWQAAAPs/shirley-temple-lol.webm" + } + ], + "gifs": [ + { + "id": "13011029513751011075", + "title": "", + "url": "https://tenor.com/view/feliz-dia-de-reyes-happy-three-kings-day-epiphany-3kings-day-magos-or-el-d%C3%ADa-de-reyes-gif-19847132", + "src": "https://media.tenor.com/tJB2aElAnwMAAAPs/feliz-dia-de-reyes-happy-three-kings-day.webm", + "gif_src": "https://media.tenor.com/tJB2aElAnwMAAAAC/feliz-dia-de-reyes-happy-three-kings-day.gif", + "width": 640, + "height": 640, + "preview": "https://media.tenor.com/tJB2aElAnwMAAAAD/feliz-dia-de-reyes-happy-three-kings-day.png" + } + ] +} diff --git a/Backend/package-lock.json b/Backend/package-lock.json index b8136e7..b1f3735 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -9,14 +9,32 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.13.2", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.2.1", + "livekit-server-sdk": "^2.15.0", + "multer": "^2.0.2", "pg": "^8.16.3", "redis": "^5.10.0", "socket.io": "^4.8.3" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@livekit/protocol": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.43.4.tgz", + "integrity": "sha512-mJDFt/p+G2OKmIGizYiACK7Jb06wd42m9Pe7Y9cAOfdYpvwCqHlw4yul5Z7iRU3VKPsYJ27WL3oeHEoiu+HuAA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, "node_modules/@redis/bloom": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz", @@ -114,6 +132,29 @@ "node": ">= 0.6" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -147,6 +188,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -185,6 +243,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz", + "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==", + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0", + "map-obj": "5.0.0", + "quick-lru": "^6.1.1", + "type-fest": "^4.3.2" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -194,6 +282,33 @@ "node": ">=0.10.0" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -264,6 +379,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -416,6 +540,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -495,6 +634,63 @@ "url": "https://opencollective.com/express" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -583,6 +779,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -652,6 +863,42 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/livekit-server-sdk": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.15.0.tgz", + "integrity": "sha512-HmzjWnwEwwShu8yUf7VGFXdc+BuMJR5pnIY4qsdlhqI9d9wDgq+4cdTEHg0NEBaiGnc6PCOBiaTYgmIyVJ0S9w==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.1", + "@livekit/protocol": "^1.43.1", + "camelcase-keys": "^9.0.0", + "jose": "^5.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/map-obj": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz", + "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -707,12 +954,94 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -924,6 +1253,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -939,6 +1274,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -963,6 +1310,20 @@ "node": ">= 0.10" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/redis": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz", @@ -995,6 +1356,26 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1226,6 +1607,23 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1235,6 +1633,18 @@ "node": ">=0.6" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1249,6 +1659,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -1264,6 +1680,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/Backend/package.json b/Backend/package.json index 8be2851..3f35210 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -10,9 +10,12 @@ "author": "", "license": "ISC", "dependencies": { + "axios": "^1.13.2", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.2.1", + "livekit-server-sdk": "^2.15.0", + "multer": "^2.0.2", "pg": "^8.16.3", "redis": "^5.10.0", "socket.io": "^4.8.3" diff --git a/Backend/routes/auth.js b/Backend/routes/auth.js index e0aaaca..66b75ad 100644 --- a/Backend/routes/auth.js +++ b/Backend/routes/auth.js @@ -11,14 +11,81 @@ function generateFakeSalt(username) { } router.post('/register', async (req, res) => { - const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys } = req.body; + const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys, inviteCode } = req.body; + try { - const result = await db.query( - `INSERT INTO users (username, client_salt, encrypted_master_key, hashed_auth_key, public_identity_key, public_signing_key, encrypted_private_keys) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, - [username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys] - ); - res.json({ success: true, userId: result.rows[0].id }); + // Step 1: Enforce Invite (unless first user) + const userCountRes = await db.query('SELECT count(*) FROM users'); + const userCount = parseInt(userCountRes.rows[0].count); + + if (userCount > 0) { + if (!inviteCode) { + return res.status(403).json({ error: 'Invite code required' }); + } + + // Check Invite validity + const inviteRes = await db.query('SELECT * FROM invites WHERE code = $1', [inviteCode]); + if (inviteRes.rows.length === 0) { + return res.status(403).json({ error: 'Invalid invite code' }); + } + + var invite = inviteRes.rows[0]; + + // Check Expiration + if (invite.expires_at && new Date() > new Date(invite.expires_at)) { + return res.status(410).json({ error: 'Invite expired' }); + } + + // Check Usage Limits + if (invite.max_uses !== null && invite.uses >= invite.max_uses) { + return res.status(410).json({ error: 'Invite max uses reached' }); + } + } + + // START TRANSACTION - To ensure invite usage and user creation are atomic + await db.query('BEGIN'); + + try { + // Update Invite Usage (only if enforced) + if (userCount > 0) { + await db.query('UPDATE invites SET uses = uses + 1 WHERE code = $1', [inviteCode]); + } + + // Create User + // Create User + const result = await db.query( + `INSERT INTO users (username, client_salt, encrypted_master_key, hashed_auth_key, public_identity_key, public_signing_key, encrypted_private_keys) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, + [username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys] + ); + const newUserId = result.rows[0].id; + + // Assign Roles + // 1. @everyone (Always) + await db.query(` + INSERT INTO user_roles (user_id, role_id) + SELECT $1, id FROM roles WHERE name = '@everyone' + `, [newUserId]); + + // 2. Owner (If first user or if admin logic allows) + if (userCount === 0) { + await db.query(` + INSERT INTO user_roles (user_id, role_id) + SELECT $1, id FROM roles WHERE name = 'Owner' + `, [newUserId]); + + // Also set is_admin = true for legacy support + await db.query('UPDATE users SET is_admin = TRUE WHERE id = $1', [newUserId]); + } + + await db.query('COMMIT'); + res.json({ success: true, userId: result.rows[0].id }); + + } catch (txErr) { + await db.query('ROLLBACK'); + throw txErr; + } + } catch (err) { console.error(err); if (err.code === '23505') { // Unique violation @@ -50,7 +117,7 @@ router.post('/login/verify', async (req, res) => { try { const result = await db.query( - 'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1', + 'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys, public_identity_key FROM users WHERE username = $1', [username] ); @@ -66,7 +133,8 @@ router.post('/login/verify', async (req, res) => { success: true, userId: user.id, encryptedMK: user.encrypted_master_key, - encryptedPrivateKeys: user.encrypted_private_keys + encryptedPrivateKeys: user.encrypted_private_keys, + publicKey: user.public_identity_key // Return Public Key }); } else { res.status(401).json({ error: 'Invalid credentials' }); @@ -77,4 +145,14 @@ router.post('/login/verify', async (req, res) => { } }); +router.get('/users/public-keys', async (req, res) => { + try { + const result = await db.query('SELECT id, public_identity_key FROM users'); + res.json(result.rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + module.exports = router; diff --git a/Backend/routes/channels.js b/Backend/routes/channels.js index ce319cb..d2cf1e3 100644 --- a/Backend/routes/channels.js +++ b/Backend/routes/channels.js @@ -12,4 +12,149 @@ router.get('/', async (req, res) => { } }); +// Create New Channel +router.post('/create', async (req, res) => { + console.log('Creates Channel Body:', req.body); + const { name, type } = req.body; + if (!name) return res.status(400).json({ error: 'Channel name required' }); + + try { + const result = await db.query( + 'INSERT INTO channels (name, type) VALUES ($1, $2) RETURNING *', + [name, type || 'text'] + ); + const newChannel = result.rows[0]; + // DO NOT emit 'new_channel' here. Wait until keys are uploaded. + res.json({ id: newChannel.id }); + } catch (err) { + console.error('Error creating channel:', err); + if (err.code === '23505') { + res.status(400).json({ error: 'Channel already exists' }); + } else { + res.status(500).json({ error: 'Server error' }); + } + } +}); + +// Notify Channel Creation (Called AFTER keys are uploaded) +router.post('/:id/notify', async (req, res) => { + const { id } = req.params; + try { + const result = await db.query('SELECT * FROM channels WHERE id = $1', [id]); + if (result.rows.length === 0) return res.status(404).json({ error: 'Channel not found' }); + + const channel = result.rows[0]; + if (req.io) req.io.emit('new_channel', channel); // Emit NOW + + res.json({ success: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Upload Channel Keys (for self or others) +// Upload Channel Keys (for self or others) - Supports Batch +router.post('/keys', async (req, res) => { + // Check if body is array + const keysToUpload = Array.isArray(req.body) ? req.body : [req.body]; + + if (keysToUpload.length === 0) return res.json({ success: true }); + + try { + await db.query('BEGIN'); + + for (const keyData of keysToUpload) { + const { channelId, userId, encryptedKeyBundle, keyVersion } = keyData; + + if (!channelId || !userId || !encryptedKeyBundle) { + continue; + } + + await db.query( + `INSERT INTO channel_keys (channel_id, user_id, encrypted_key_bundle, key_version) + VALUES ($1, $2, $3, $4) + ON CONFLICT (channel_id, user_id) DO UPDATE + SET encrypted_key_bundle = EXCLUDED.encrypted_key_bundle, + key_version = EXCLUDED.key_version`, + [channelId, userId, encryptedKeyBundle, keyVersion || 1] + ); + } + + await db.query('COMMIT'); + res.json({ success: true, count: keysToUpload.length }); + } catch (err) { + await db.query('ROLLBACK'); + console.error('Error uploading channel keys:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Get User's Channel Keys +router.get('/keys/:userId', async (req, res) => { + const { userId } = req.params; + try { + const result = await db.query( + 'SELECT channel_id, encrypted_key_bundle, key_version FROM channel_keys WHERE user_id = $1', + [userId] + ); + res.json(result.rows); + } catch (err) { + console.error('Error fetching channel keys:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Update Channel Name +router.put('/:id', async (req, res) => { + const { id } = req.params; + const { name } = req.body; + + if (!name) return res.status(400).json({ error: 'Name required' }); + + try { + const result = await db.query( + 'UPDATE channels SET name = $1 WHERE id = $2 RETURNING *', + [name, id] + ); + if (result.rows.length === 0) return res.status(404).json({ error: 'Channel not found' }); + + const updatedChannel = result.rows[0]; + if (req.io) req.io.emit('channel_renamed', updatedChannel); + res.json(updatedChannel); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Delete Channel +router.delete('/:id', async (req, res) => { + const { id } = req.params; + + try { + await db.query('BEGIN'); + + // Manual Cascade (since we didn't set FK CASCADE in schema for these yet) + await db.query('DELETE FROM messages WHERE channel_id = $1', [id]); + await db.query('DELETE FROM channel_keys WHERE channel_id = $1', [id]); + + const result = await db.query('DELETE FROM channels WHERE id = $1 RETURNING *', [id]); + + if (result.rows.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ error: 'Channel not found' }); + } + + await db.query('COMMIT'); + + if (req.io) req.io.emit('channel_deleted', id); + res.json({ success: true, deletedId: id }); + } catch (err) { + await db.query('ROLLBACK'); + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + module.exports = router; diff --git a/Backend/routes/invites.js b/Backend/routes/invites.js new file mode 100644 index 0000000..a468b32 --- /dev/null +++ b/Backend/routes/invites.js @@ -0,0 +1,75 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); + +// Create a new invite +router.post('/create', async (req, res) => { + const { code, encryptedPayload, createdBy, maxUses, expiresAt, keyVersion } = req.body; + + if (!code || !encryptedPayload || !createdBy || !keyVersion) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + await db.query( + `INSERT INTO invites (code, encrypted_payload, created_by, max_uses, expires_at, key_version) + VALUES ($1, $2, $3, $4, $5, $6)`, + [code, encryptedPayload, createdBy, maxUses || null, expiresAt || null, keyVersion] + ); + res.json({ success: true }); + } catch (err) { + console.error('Error creating invite:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Fetch an invite (and validate it) +router.get('/:code', async (req, res) => { + const { code } = req.params; + + try { + const result = await db.query('SELECT * FROM invites WHERE code = $1', [code]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Invite not found' }); + } + + const invite = result.rows[0]; + + // Check Expiration + if (invite.expires_at && new Date() > new Date(invite.expires_at)) { + return res.status(410).json({ error: 'Invite expired' }); + } + + // Check Usage Limits + if (invite.max_uses !== null && invite.uses >= invite.max_uses) { + return res.status(410).json({ error: 'Invite max uses reached' }); + } + + // Increment Uses + await db.query('UPDATE invites SET uses = uses + 1 WHERE code = $1', [code]); + + res.json({ + encryptedPayload: invite.encrypted_payload, + keyVersion: invite.key_version + }); + + } catch (err) { + console.error('Error fetching invite:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Delete an invite (Revoke) -> Triggers client-side key rotation policy warning? +// The client should call this, then rotate keys. +router.delete('/:code', async (req, res) => { + const { code } = req.params; + try { + await db.query('DELETE FROM invites WHERE code = $1', [code]); + res.json({ success: true }); + } catch (err) { + console.error('Error deleting invite:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + +module.exports = router; diff --git a/Backend/routes/roles.js b/Backend/routes/roles.js new file mode 100644 index 0000000..009f3d6 --- /dev/null +++ b/Backend/routes/roles.js @@ -0,0 +1,213 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); + +// Middleware to check for permissions (simplified for now) +// In a real app, you'd check if req.user has the permission +// Middleware to check for permissions +const checkPermission = (requiredPerm) => { + return async (req, res, next) => { + const userId = req.headers['x-user-id']; + + if (!userId) { + return res.status(401).json({ error: 'Unauthorized: No User ID' }); + } + + try { + // Fetch all roles for user and aggregate permissions + const result = await db.query(` + SELECT r.permissions + FROM user_roles ur + JOIN roles r ON ur.role_id = r.id + WHERE ur.user_id = $1 + `, [userId]); + + let hasPermission = false; + + // Check if ANY of the user's roles has the permission + for (const row of result.rows) { + const perms = row.permissions || {}; + // If Manage Roles is true, or if checking for something else and they have it + if (perms[requiredPerm] === true) { + hasPermission = true; + break; + } + // Implicit Admin/Owner check: if they have 'manage_roles' and we are checking something lower? + // For "Owner" role, we seeded it with specific true values. + // But let's check for 'administrator' equivalent if we had it. + // For now, implicit check: if we need 'manage_roles', look for it. + } + + if (!hasPermission) { + return res.status(403).json({ error: `Forbidden: Missing ${requiredPerm}` }); + } + + next(); + } catch (err) { + console.error('Permission check failed:', err); + return res.status(500).json({ error: 'Internal Server Error' }); + } + }; +}; + +// GET /api/roles/permissions - Get current user's permissions +router.get('/permissions', async (req, res) => { + const userId = req.headers['x-user-id']; + if (!userId) return res.json({}); // No perms if no ID + + try { + const result = await db.query(` + SELECT r.permissions + FROM user_roles ur + JOIN roles r ON ur.role_id = r.id + WHERE ur.user_id = $1 + `, [userId]); + + const finalPerms = { + manage_channels: false, + manage_roles: false, + create_invite: false, + embed_links: false, + attach_files: false + }; + + for (const row of result.rows) { + const p = row.permissions || {}; + if (p.manage_channels) finalPerms.manage_channels = true; + if (p.manage_roles) finalPerms.manage_roles = true; + if (p.create_invite) finalPerms.create_invite = true; + if (p.embed_links) finalPerms.embed_links = true; + if (p.attach_files) finalPerms.attach_files = true; + } + res.json(finalPerms); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// GET /api/roles - List all roles +router.get('/', async (req, res) => { + try { + const result = await db.query('SELECT * FROM roles ORDER BY position DESC, id ASC'); + res.json(result.rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// POST /api/roles - Create new role +router.post('/', checkPermission('manage_roles'), async (req, res) => { + const { name, color, permissions, position, is_hoist } = req.body; + try { + const result = await db.query( + 'INSERT INTO roles (name, color, permissions, position, is_hoist) VALUES ($1, $2, $3, $4, $5) RETURNING *', + [name || 'new role', color || '#99aab5', permissions || {}, position || 0, is_hoist || false] + ); + res.json(result.rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// PUT /api/roles/:id - Update role +router.put('/:id', checkPermission('manage_roles'), async (req, res) => { + const { id } = req.params; + const { name, color, permissions, position, is_hoist } = req.body; + + // Dynamic update query + const fields = []; + const values = []; + let idx = 1; + + if (name !== undefined) { fields.push(`name = $${idx++}`); values.push(name); } + if (color !== undefined) { fields.push(`color = $${idx++}`); values.push(color); } + if (permissions !== undefined) { fields.push(`permissions = $${idx++}`); values.push(permissions); } + if (position !== undefined) { fields.push(`position = $${idx++}`); values.push(position); } + if (is_hoist !== undefined) { fields.push(`is_hoist = $${idx++}`); values.push(is_hoist); } + + if (fields.length === 0) return res.json({ success: true }); // Nothing to update + + values.push(id); + const query = `UPDATE roles SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`; + + try { + const result = await db.query(query, values); + if (result.rows.length === 0) return res.status(404).json({ error: 'Role not found' }); + res.json(result.rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// DELETE /api/roles/:id - Delete role +router.delete('/:id', checkPermission('manage_roles'), async (req, res) => { + const { id } = req.params; + try { + // user_roles cascade handled by DB + const result = await db.query('DELETE FROM roles WHERE id = $1 RETURNING *', [id]); + if (result.rows.length === 0) return res.status(404).json({ error: 'Role not found' }); + res.json({ success: true, deletedId: id }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// GET /api/roles/members - List members with roles +// (This is effectively GET /api/users but enriched with roles) +router.get('/members', async (req, res) => { + try { + const result = await db.query(` + SELECT u.id, u.username, u.public_identity_key, + json_agg(r.*) FILTER (WHERE r.id IS NOT NULL) as roles + FROM users u + LEFT JOIN user_roles ur ON u.id = ur.user_id + LEFT JOIN roles r ON ur.role_id = r.id + GROUP BY u.id + `); + res.json(result.rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// POST /api/roles/:id/assign - Assign role to user +router.post('/:id/assign', checkPermission('manage_roles'), async (req, res) => { + const { id } = req.params; // role id + const { userId } = req.body; + + try { + await db.query( + 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', + [userId, id] + ); + res.json({ success: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// POST /api/roles/:id/remove - Remove role from user +router.post('/:id/remove', checkPermission('manage_roles'), async (req, res) => { + const { id } = req.params; // role id + const { userId } = req.body; + + try { + await db.query( + 'DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2', + [userId, id] + ); + res.json({ success: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +module.exports = router; diff --git a/Backend/routes/upload.js b/Backend/routes/upload.js new file mode 100644 index 0000000..2d10cee --- /dev/null +++ b/Backend/routes/upload.js @@ -0,0 +1,41 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +const router = express.Router(); + +// Ensure uploads directory exists +const uploadDir = path.join(__dirname, '../uploads'); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir); +} + +// Configure Multer Storage +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + // Generate unique filename: timestamp-random.ext + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + cb(null, uniqueSuffix + ext); + } +}); + +const upload = multer({ storage: storage }); + +// POST /api/upload +router.post('/', upload.single('file'), (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + + // Return the URL to access the file + // Assumes server serves 'uploads' folder at '/uploads' + const fileUrl = `/uploads/${req.file.filename}`; + res.json({ url: fileUrl, filename: req.file.filename }); +}); + +module.exports = router; diff --git a/Backend/routes/voice.js b/Backend/routes/voice.js new file mode 100644 index 0000000..1268c14 --- /dev/null +++ b/Backend/routes/voice.js @@ -0,0 +1,64 @@ +const express = require('express'); +const router = express.Router(); +const { AccessToken } = require('livekit-server-sdk'); +const db = require('../db'); + +// Middleware to check permissions? +// For now, simpler: assuming user is logged in (via x-user-id header check in frontend) +// Real implementation should use the checkPermission middleware or verify session + +router.post('/token', async (req, res) => { + const { channelId } = req.body; + const userId = req.headers['x-user-id']; // Sent by frontend + + // Default fallback if no user (should rely on auth middleware ideally) + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + try { + // 1. Get Username (for display) + const userRes = await db.query('SELECT username FROM users WHERE id = $1', [userId]); + if (userRes.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + const username = userRes.rows[0].username; + + // 2. Get Channel Name (Optional, for room name check) + // Ensure channel exists and is of type 'voice' + const channelRes = await db.query('SELECT id, type FROM channels WHERE id = $1', [channelId]); + if (channelRes.rows.length === 0) { + return res.status(404).json({ error: 'Channel not found' }); + } + if (channelRes.rows[0].type !== 'voice') { + return res.status(400).json({ error: 'Not a voice channel' }); + } + + // 3. Generate Token + // API Key/Secret from env + const apiKey = process.env.LIVEKIT_API_KEY || 'devkey'; + const apiSecret = process.env.LIVEKIT_API_SECRET || 'secret'; + + const at = new AccessToken(apiKey, apiSecret, { + identity: userId, + name: username, + }); + + at.addGrant({ + roomJoin: true, + room: channelId, + canPublish: true, + canSubscribe: true, + }); + + const token = await at.toJwt(); + + res.json({ token }); + + } catch (err) { + console.error('Error creating voice token:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + +module.exports = router; diff --git a/Backend/schema.sql b/Backend/schema.sql index d36e0bf..16a3251 100644 --- a/Backend/schema.sql +++ b/Backend/schema.sql @@ -8,22 +8,32 @@ CREATE TABLE IF NOT EXISTS users ( hashed_auth_key TEXT NOT NULL, public_identity_key TEXT NOT NULL, public_signing_key TEXT NOT NULL, - encrypted_private_keys TEXT NOT NULL, -- Added this column + encrypted_private_keys TEXT NOT NULL, is_admin BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT NOW() + created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT UNIQUE NOT NULL, - type TEXT DEFAULT 'text', - created_at TIMESTAMP DEFAULT NOW() + type TEXT DEFAULT 'text' CHECK (type IN ('text', 'voice')), + created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS roles ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, - permissions JSONB + color TEXT DEFAULT '#99aab5', + position INTEGER DEFAULT 0, + permissions JSONB DEFAULT '{}', + is_hoist BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS user_roles ( + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, role_id) ); CREATE TABLE IF NOT EXISTS channel_keys ( @@ -42,5 +52,16 @@ CREATE TABLE IF NOT EXISTS messages ( nonce TEXT NOT NULL, signature TEXT NOT NULL, key_version INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT NOW() + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS invites ( + code TEXT PRIMARY KEY, -- e.g. "8f1a4c..." (The ID of the invite) + encrypted_payload TEXT NOT NULL, -- The AES-Encrypted Key Bundle (Server can't read this) + created_by UUID REFERENCES users(id) ON DELETE CASCADE, + max_uses INTEGER DEFAULT NULL, -- NULL = Infinite + uses INTEGER DEFAULT 0, + expires_at TIMESTAMPTZ DEFAULT NULL, -- NULL = Never + key_version INTEGER NOT NULL, -- Which key version is inside? (So we can invalidate leaks) + created_at TIMESTAMPTZ DEFAULT NOW() ); diff --git a/Backend/scripts/init-db.js b/Backend/scripts/init-db.js index 641a4d6..747861a 100644 --- a/Backend/scripts/init-db.js +++ b/Backend/scripts/init-db.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '../.env') }); const db = require('../db'); async function initDb() { @@ -7,18 +8,33 @@ async function initDb() { const schemaPath = path.join(__dirname, '../schema.sql'); const schemaSql = fs.readFileSync(schemaPath, 'utf8'); + console.log('Dropping existing tables...'); + await db.query(` + DROP TABLE IF EXISTS invites CASCADE; + DROP TABLE IF EXISTS messages CASCADE; + DROP TABLE IF EXISTS channel_keys CASCADE; + DROP TABLE IF EXISTS roles CASCADE; + DROP TABLE IF EXISTS channels CASCADE; + DROP TABLE IF EXISTS users CASCADE; + `); + console.log('Applying schema...'); await db.query(schemaSql); - // Seed Channels - const channels = ['general', 'random']; - for (const name of channels) { - await db.query( - `INSERT INTO channels (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`, - [name] - ); - } - console.log('Channels seeded.'); + console.log('Schema applied successfully.'); + + console.log('Seeding Default Roles...'); + // 1. @everyone (Limited) + await db.query(` + INSERT INTO roles (name, color, position, is_hoist, permissions) + VALUES ('@everyone', '#99aab5', 0, false, '{"manage_channels": false, "manage_roles": false, "create_invite": false, "embed_links": true, "attach_files": true}') + `); + + // 2. Owner (All Permissions) + await db.query(` + INSERT INTO roles (name, color, position, is_hoist, permissions) + VALUES ('Owner', '#ED4245', 100, true, '{"manage_channels": true, "manage_roles": true, "create_invite": true, "embed_links": true, "attach_files": true}') + `); console.log('Schema applied successfully.'); process.exit(0); diff --git a/Backend/scripts/migrate_voice.js b/Backend/scripts/migrate_voice.js new file mode 100644 index 0000000..2808bcd --- /dev/null +++ b/Backend/scripts/migrate_voice.js @@ -0,0 +1,17 @@ +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '../.env') }); +const db = require('../db'); + +async function migrate() { + try { + console.log('Running migration...'); + await db.query("ALTER TABLE channels ADD COLUMN IF NOT EXISTS type TEXT DEFAULT 'text' CHECK (type IN ('text', 'voice'))"); + console.log('Migration complete: Added type column to channels.'); + process.exit(0); + } catch (err) { + console.error('Migration failed:', err); + process.exit(1); + } +} + +migrate(); diff --git a/Backend/server.js b/Backend/server.js index f3e1357..f7cac46 100644 --- a/Backend/server.js +++ b/Backend/server.js @@ -2,6 +2,7 @@ const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const cors = require('cors'); +const axios = require('axios'); require('dotenv').config(); const app = express(); @@ -15,36 +16,104 @@ const io = new Server(server, { const authRoutes = require('./routes/auth'); const channelRoutes = require('./routes/channels'); +const uploadRoutes = require('./routes/upload'); +const inviteRoutes = require('./routes/invites'); +const rolesRoutes = require('./routes/roles'); app.use(cors()); app.use(express.json()); +// Attach IO to request +app.use((req, res, next) => { + req.io = io; + next(); +}); + +// Serve uploads folder based on relative path from server.js +const path = require('path'); +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + app.use('/api/auth', authRoutes); app.use('/api/channels', channelRoutes); +app.use('/api/upload', uploadRoutes); +app.use('/api/invites', inviteRoutes); +app.use('/api/invites', inviteRoutes); +app.use('/api/roles', rolesRoutes); +app.use('/api/voice', require('./routes/voice')); app.get('/', (req, res) => { res.send('Secure Chat Backend Running'); }); +// GIF Routes +const gifCategories = require('./data/gif_categories.json'); + +app.get('/api/gifs/categories', (req, res) => { + res.json(gifCategories); +}); + +app.get('/api/gifs/search', async (req, res) => { + const { q, limit = 8 } = req.query; + const apiKey = process.env.TENOR_API_KEY; + + if (!apiKey) { + // Return mock data or error if no key + console.warn('TENOR_API_KEY missing in .env'); + // Return dummy response to prevent crash + return res.json({ results: [] }); + } + + try { + const response = await axios.get(`https://tenor.googleapis.com/v2/search`, { + params: { + q, + key: apiKey, + limit + } + }); + res.json(response.data); + } catch (err) { + console.error('Tenor API Error:', err.message); + res.status(500).json({ error: 'Failed to fetch GIFs' }); + } +}); + const redisClient = require('./redis'); const db = require('./db'); io.on('connection', (socket) => { console.log('User connected:', socket.id); - socket.on('join_channel', async (channelId) => { + socket.on('join_channel', async (data) => { + // Handle both simple string (legacy) and object payload + const channelId = typeof data === 'object' ? data.channelId : data; + const userId = typeof data === 'object' ? data.userId : null; + socket.join(channelId); - console.log(`User ${socket.id} joined channel ${channelId}`); - // Load recent messages + console.log(`User ${socket.id} (ID: ${userId}) joined channel ${channelId}`); + // Load recent messages with reactions try { - const result = await db.query( - `SELECT m.*, u.username, u.public_signing_key - FROM messages m - JOIN users u ON m.sender_id = u.id - WHERE m.channel_id = $1 - ORDER BY m.created_at DESC LIMIT 50`, - [channelId] - ); + const query = ` + SELECT m.*, u.username, u.public_signing_key, + ( + SELECT json_object_agg(res.emoji, res.data) + FROM ( + SELECT r.emoji, json_build_object( + 'count', COUNT(*)::int, + 'me', BOOL_OR(r.user_id = $2) + ) as data + FROM message_reactions r + WHERE r.message_id = m.id + GROUP BY r.emoji + ) res + ) as reactions + FROM messages m + JOIN users u ON m.sender_id = u.id + WHERE m.channel_id = $1 + ORDER BY m.created_at DESC LIMIT 50 + `; + + const result = await db.query(query, [channelId, userId]); socket.emit('recent_messages', result.rows.reverse()); } catch (err) { console.error('Error fetching messages:', err); @@ -83,12 +152,114 @@ io.on('connection', (socket) => { } }); - socket.on('typing', (data) => { - socket.to(data.channelId).emit('user_typing', { username: data.username }); + socket.on('typing_start', (data) => { + io.to(data.channelId).emit('typing_start', data); + }); + + socket.on('typing_stop', (data) => { + io.to(data.channelId).emit('typing_stop', data); + }); + + // ... Message handling ... + + // Voice State Handling + // Stores: channelId -> Set(serializedUser: {userId, username}) + // For simplicity: channelId -> [{userId, username}] + + // We need a global variable outside the connection loop, but for this file structure 'socket.on' is inside io.on + // So we need to define it outside. + // See defining it at top of file logic. + + + // Reactions + socket.on('add_reaction', async ({ channelId, messageId, userId, emoji }) => { + try { + if (!messageId || !userId || !emoji) return; + + const result = await db.query( + `INSERT INTO message_reactions (message_id, user_id, emoji) + VALUES ($1, $2, $3) + ON CONFLICT (message_id, user_id, emoji) DO NOTHING`, + [messageId, userId, emoji] + ); + + if (result.rowCount > 0) { + io.to(channelId).emit('reaction_added', { messageId, userId, emoji }); + } + } catch (err) { + console.error('Error adding reaction:', err); + } + }); + + socket.on('remove_reaction', async ({ channelId, messageId, userId, emoji }) => { + try { + if (!messageId || !userId || !emoji) return; + + const result = await db.query( + `DELETE FROM message_reactions + WHERE message_id = $1 AND user_id = $2 AND emoji = $3`, + [messageId, userId, emoji] + ); + + if (result.rowCount > 0) { + io.to(channelId).emit('reaction_removed', { messageId, userId, emoji }); + } + } catch (err) { + console.error('Error removing reaction:', err); + } + }); + + socket.on('join_voice', (channelId) => { }); // Deprecated/Unused placeholder + + socket.on('voice_state_change', (data) => { + // data: { channelId, userId, username, action: 'joined' | 'left' | 'state_update', isMuted, isDeafened } + console.log(`Voice State Update: ${data.username} (${data.userId}) ${data.action} ${data.channelId}`); + + // Update Server State + if (!global.voiceStates) global.voiceStates = {}; + + const currentUsers = global.voiceStates[data.channelId] || []; + + if (data.action === 'joined') { + const existingUser = currentUsers.find(u => u.userId === data.userId); + if (!existingUser) { + currentUsers.push({ + userId: data.userId, + username: data.username, + isMuted: data.isMuted || false, + isDeafened: data.isDeafened || false + }); + } else { + // Update existing on re-join (or just state sync) + existingUser.isMuted = data.isMuted || false; + existingUser.isDeafened = data.isDeafened || false; + } + global.voiceStates[data.channelId] = currentUsers; + } else if (data.action === 'left') { + const index = currentUsers.findIndex(u => u.userId === data.userId); + if (index !== -1) { + currentUsers.splice(index, 1); + global.voiceStates[data.channelId] = currentUsers; + } + } else if (data.action === 'state_update') { + const user = currentUsers.find(u => u.userId === data.userId); + if (user) { + if (data.isMuted !== undefined) user.isMuted = data.isMuted; + if (data.isDeafened !== undefined) user.isDeafened = data.isDeafened; + global.voiceStates[data.channelId] = currentUsers; + } + } + + io.emit('voice_state_update', data); + }); + + socket.on('request_voice_state', () => { + socket.emit('full_voice_state', global.voiceStates || {}); }); socket.on('disconnect', () => { console.log('User disconnected:', socket.id); + // TODO: Auto-leave logic would require mapping socketId -> userId/channelId }); }); diff --git a/Backend/uploads/1767493519909-231561716.enc b/Backend/uploads/1767493519909-231561716.enc new file mode 100644 index 0000000..47cb9c3 Binary files /dev/null and b/Backend/uploads/1767493519909-231561716.enc differ diff --git a/Backend/uploads/1767493677800-686548739.enc b/Backend/uploads/1767493677800-686548739.enc new file mode 100644 index 0000000..fa9b320 Binary files /dev/null and b/Backend/uploads/1767493677800-686548739.enc differ diff --git a/Backend/uploads/1767496855215-848429954.enc b/Backend/uploads/1767496855215-848429954.enc new file mode 100644 index 0000000..8ab029b Binary files /dev/null and b/Backend/uploads/1767496855215-848429954.enc differ diff --git a/Backend/uploads/1767497306694-744544719.enc b/Backend/uploads/1767497306694-744544719.enc new file mode 100644 index 0000000..7868869 Binary files /dev/null and b/Backend/uploads/1767497306694-744544719.enc differ diff --git a/Backend/uploads/1767571577033-791228774.enc b/Backend/uploads/1767571577033-791228774.enc new file mode 100644 index 0000000..4e13e0b Binary files /dev/null and b/Backend/uploads/1767571577033-791228774.enc differ diff --git a/Backend/uploads/1767731525251-885122068.enc b/Backend/uploads/1767731525251-885122068.enc new file mode 100644 index 0000000..3d58ca7 Binary files /dev/null and b/Backend/uploads/1767731525251-885122068.enc differ diff --git a/Backend/uploads/1767736657939-176839166.enc b/Backend/uploads/1767736657939-176839166.enc new file mode 100644 index 0000000..7a4720c Binary files /dev/null and b/Backend/uploads/1767736657939-176839166.enc differ diff --git a/Backend/uploads/1767736668121-484246219.enc b/Backend/uploads/1767736668121-484246219.enc new file mode 100644 index 0000000..27a6e79 Binary files /dev/null and b/Backend/uploads/1767736668121-484246219.enc differ diff --git a/Frontend/Electron/dist-react/assets/accordion-BPueGNgN.svg b/Frontend/Electron/dist-react/assets/accordion-BPueGNgN.svg new file mode 100644 index 0000000..c9c21ca --- /dev/null +++ b/Frontend/Electron/dist-react/assets/accordion-BPueGNgN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/anatomical_heart-DbQDqK_8.svg b/Frontend/Electron/dist-react/assets/anatomical_heart-DbQDqK_8.svg new file mode 100644 index 0000000..e6916d2 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/anatomical_heart-DbQDqK_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/brain-Czvux5Q4.svg b/Frontend/Electron/dist-react/assets/brain-Czvux5Q4.svg new file mode 100644 index 0000000..653427d --- /dev/null +++ b/Frontend/Electron/dist-react/assets/brain-Czvux5Q4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/bubble_tea-Cy1d5egt.svg b/Frontend/Electron/dist-react/assets/bubble_tea-Cy1d5egt.svg new file mode 100644 index 0000000..8cb6178 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/bubble_tea-Cy1d5egt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/burrito-B4L0kbwK.svg b/Frontend/Electron/dist-react/assets/burrito-B4L0kbwK.svg new file mode 100644 index 0000000..c76d82c --- /dev/null +++ b/Frontend/Electron/dist-react/assets/burrito-B4L0kbwK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/butterfly-AxzpD-Pg.svg b/Frontend/Electron/dist-react/assets/butterfly-AxzpD-Pg.svg new file mode 100644 index 0000000..22c6ead --- /dev/null +++ b/Frontend/Electron/dist-react/assets/butterfly-AxzpD-Pg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/coat-Cbu3wnI6.svg b/Frontend/Electron/dist-react/assets/coat-Cbu3wnI6.svg new file mode 100644 index 0000000..392af56 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/coat-Cbu3wnI6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/couple-KSrP6fk0.svg b/Frontend/Electron/dist-react/assets/couple-KSrP6fk0.svg new file mode 100644 index 0000000..59ca8c0 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/couple-KSrP6fk0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/crab-D6qU1zIW.svg b/Frontend/Electron/dist-react/assets/crab-D6qU1zIW.svg new file mode 100644 index 0000000..8f45b53 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/crab-D6qU1zIW.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/cucumber-oVkPYVB9.svg b/Frontend/Electron/dist-react/assets/cucumber-oVkPYVB9.svg new file mode 100644 index 0000000..83cba03 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/cucumber-oVkPYVB9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/deafen-BWE6ozKl.mp3 b/Frontend/Electron/dist-react/assets/deafen-BWE6ozKl.mp3 new file mode 100644 index 0000000..8e732e1 Binary files /dev/null and b/Frontend/Electron/dist-react/assets/deafen-BWE6ozKl.mp3 differ diff --git a/Frontend/Electron/dist-react/assets/dodo-CoZFlciJ.svg b/Frontend/Electron/dist-react/assets/dodo-CoZFlciJ.svg new file mode 100644 index 0000000..1dbac1e --- /dev/null +++ b/Frontend/Electron/dist-react/assets/dodo-CoZFlciJ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/emojies_colored-Cxo2u_zo.png b/Frontend/Electron/dist-react/assets/emojies_colored-Cxo2u_zo.png new file mode 100644 index 0000000..fd8472a Binary files /dev/null and b/Frontend/Electron/dist-react/assets/emojies_colored-Cxo2u_zo.png differ diff --git a/Frontend/Electron/dist-react/assets/emojies_greyscale-CtRIvx0g.png b/Frontend/Electron/dist-react/assets/emojies_greyscale-CtRIvx0g.png new file mode 100644 index 0000000..7e304af Binary files /dev/null and b/Frontend/Electron/dist-react/assets/emojies_greyscale-CtRIvx0g.png differ diff --git a/Frontend/Electron/dist-react/assets/empty_nest-DGy7reBo.svg b/Frontend/Electron/dist-react/assets/empty_nest-DGy7reBo.svg new file mode 100644 index 0000000..d27cf19 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/empty_nest-DGy7reBo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/face_in_clouds-DBzCKo8S.svg b/Frontend/Electron/dist-react/assets/face_in_clouds-DBzCKo8S.svg new file mode 100644 index 0000000..dc0a474 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/face_in_clouds-DBzCKo8S.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/ferris_wheel-DvW0t9g3.svg b/Frontend/Electron/dist-react/assets/ferris_wheel-DvW0t9g3.svg new file mode 100644 index 0000000..c35744a --- /dev/null +++ b/Frontend/Electron/dist-react/assets/ferris_wheel-DvW0t9g3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ac-Dr8n8VBW.svg b/Frontend/Electron/dist-react/assets/flag_ac-Dr8n8VBW.svg new file mode 100644 index 0000000..53f90dc --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ac-Dr8n8VBW.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ad-CYOJPtjR.svg b/Frontend/Electron/dist-react/assets/flag_ad-CYOJPtjR.svg new file mode 100644 index 0000000..be10594 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ad-CYOJPtjR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_af-CN78RMpg.svg b/Frontend/Electron/dist-react/assets/flag_af-CN78RMpg.svg new file mode 100644 index 0000000..769efca --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_af-CN78RMpg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_al-D439po3l.svg b/Frontend/Electron/dist-react/assets/flag_al-D439po3l.svg new file mode 100644 index 0000000..2c8655d --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_al-D439po3l.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_as-B43i20pO.svg b/Frontend/Electron/dist-react/assets/flag_as-B43i20pO.svg new file mode 100644 index 0000000..8b27532 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_as-B43i20pO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_bl-BoaeaHPp.svg b/Frontend/Electron/dist-react/assets/flag_bl-BoaeaHPp.svg new file mode 100644 index 0000000..9d4904d --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_bl-BoaeaHPp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_bm-CzSakp_Z.svg b/Frontend/Electron/dist-react/assets/flag_bm-CzSakp_Z.svg new file mode 100644 index 0000000..5e7b7f6 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_bm-CzSakp_Z.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_bo-B7hNQ755.svg b/Frontend/Electron/dist-react/assets/flag_bo-B7hNQ755.svg new file mode 100644 index 0000000..ad0a8c9 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_bo-B7hNQ755.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_bt-COHVTZ6I.svg b/Frontend/Electron/dist-react/assets/flag_bt-COHVTZ6I.svg new file mode 100644 index 0000000..e822f94 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_bt-COHVTZ6I.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_bz-B34xZjVJ.svg b/Frontend/Electron/dist-react/assets/flag_bz-B34xZjVJ.svg new file mode 100644 index 0000000..6f43e4a --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_bz-B34xZjVJ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_cy-JKjUtxO9.svg b/Frontend/Electron/dist-react/assets/flag_cy-JKjUtxO9.svg new file mode 100644 index 0000000..19bead4 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_cy-JKjUtxO9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_dg-DwJEN7pv.svg b/Frontend/Electron/dist-react/assets/flag_dg-DwJEN7pv.svg new file mode 100644 index 0000000..565a7aa --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_dg-DwJEN7pv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_do-sBcfT32z.svg b/Frontend/Electron/dist-react/assets/flag_do-sBcfT32z.svg new file mode 100644 index 0000000..c627c34 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_do-sBcfT32z.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_fj-B2-D6gPQ.svg b/Frontend/Electron/dist-react/assets/flag_fj-B2-D6gPQ.svg new file mode 100644 index 0000000..190134b --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_fj-B2-D6gPQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_fk-1KKBtSFw.svg b/Frontend/Electron/dist-react/assets/flag_fk-1KKBtSFw.svg new file mode 100644 index 0000000..0091bc7 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_fk-1KKBtSFw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_gp-DW1UVBGw.svg b/Frontend/Electron/dist-react/assets/flag_gp-DW1UVBGw.svg new file mode 100644 index 0000000..ca9e4c6 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_gp-DW1UVBGw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_gq-B3TFx5qI.svg b/Frontend/Electron/dist-react/assets/flag_gq-B3TFx5qI.svg new file mode 100644 index 0000000..d4e7119 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_gq-B3TFx5qI.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_gs-DhFNtBGF.svg b/Frontend/Electron/dist-react/assets/flag_gs-DhFNtBGF.svg new file mode 100644 index 0000000..d8b1e5f --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_gs-DhFNtBGF.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_gt-CietPgvg.svg b/Frontend/Electron/dist-react/assets/flag_gt-CietPgvg.svg new file mode 100644 index 0000000..fea623c --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_gt-CietPgvg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_gu-CyZZwWUz.svg b/Frontend/Electron/dist-react/assets/flag_gu-CyZZwWUz.svg new file mode 100644 index 0000000..2098ecc --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_gu-CyZZwWUz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_hk-CzNuCBPg.svg b/Frontend/Electron/dist-react/assets/flag_hk-CzNuCBPg.svg new file mode 100644 index 0000000..ef5ca3b --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_hk-CzNuCBPg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ht-nORDdDQL.svg b/Frontend/Electron/dist-react/assets/flag_ht-nORDdDQL.svg new file mode 100644 index 0000000..8ccca42 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ht-nORDdDQL.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ic-BrB5Xakj.svg b/Frontend/Electron/dist-react/assets/flag_ic-BrB5Xakj.svg new file mode 100644 index 0000000..46b0949 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ic-BrB5Xakj.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_je-CGBxZBdT.svg b/Frontend/Electron/dist-react/assets/flag_je-CGBxZBdT.svg new file mode 100644 index 0000000..a17c379 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_je-CGBxZBdT.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_kg-D_P2G_Do.svg b/Frontend/Electron/dist-react/assets/flag_kg-D_P2G_Do.svg new file mode 100644 index 0000000..2616d9e --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_kg-D_P2G_Do.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ki-Ccc3Xi24.svg b/Frontend/Electron/dist-react/assets/flag_ki-Ccc3Xi24.svg new file mode 100644 index 0000000..233cce8 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ki-Ccc3Xi24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ky-E8sT-Yzf.svg b/Frontend/Electron/dist-react/assets/flag_ky-E8sT-Yzf.svg new file mode 100644 index 0000000..57323f8 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ky-E8sT-Yzf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_kz-D77IkgDL.svg b/Frontend/Electron/dist-react/assets/flag_kz-D77IkgDL.svg new file mode 100644 index 0000000..d2101ab --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_kz-D77IkgDL.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_lb-DHr4ylgr.svg b/Frontend/Electron/dist-react/assets/flag_lb-DHr4ylgr.svg new file mode 100644 index 0000000..4271b73 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_lb-DHr4ylgr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_mo-PAf1BQIO.svg b/Frontend/Electron/dist-react/assets/flag_mo-PAf1BQIO.svg new file mode 100644 index 0000000..790900e --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_mo-PAf1BQIO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_mp-Bs0Xr_ND.svg b/Frontend/Electron/dist-react/assets/flag_mp-Bs0Xr_ND.svg new file mode 100644 index 0000000..f0a5fb4 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_mp-Bs0Xr_ND.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ms-BKjfidu-.svg b/Frontend/Electron/dist-react/assets/flag_ms-BKjfidu-.svg new file mode 100644 index 0000000..04a1cc1 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ms-BKjfidu-.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_mx-g-aNhK9D.svg b/Frontend/Electron/dist-react/assets/flag_mx-g-aNhK9D.svg new file mode 100644 index 0000000..93d54c4 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_mx-g-aNhK9D.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_nf-BjOIhoMF.svg b/Frontend/Electron/dist-react/assets/flag_nf-BjOIhoMF.svg new file mode 100644 index 0000000..990687f --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_nf-BjOIhoMF.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_pf-OA_PTTaZ.svg b/Frontend/Electron/dist-react/assets/flag_pf-OA_PTTaZ.svg new file mode 100644 index 0000000..333c6d0 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_pf-OA_PTTaZ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_pm-C-C2d-w4.svg b/Frontend/Electron/dist-react/assets/flag_pm-C-C2d-w4.svg new file mode 100644 index 0000000..dc55c02 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_pm-C-C2d-w4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_pn-Bde7vecB.svg b/Frontend/Electron/dist-react/assets/flag_pn-Bde7vecB.svg new file mode 100644 index 0000000..234f53f --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_pn-Bde7vecB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_rs-CmpxaRIS.svg b/Frontend/Electron/dist-react/assets/flag_rs-CmpxaRIS.svg new file mode 100644 index 0000000..5c6c69e --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_rs-CmpxaRIS.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_sa-B3EC8eCD.svg b/Frontend/Electron/dist-react/assets/flag_sa-B3EC8eCD.svg new file mode 100644 index 0000000..d0d9580 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_sa-B3EC8eCD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_sh-CT89bJZi.svg b/Frontend/Electron/dist-react/assets/flag_sh-CT89bJZi.svg new file mode 100644 index 0000000..57d004d --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_sh-CT89bJZi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_sm-BYO1ASeM.svg b/Frontend/Electron/dist-react/assets/flag_sm-BYO1ASeM.svg new file mode 100644 index 0000000..b53d00d --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_sm-BYO1ASeM.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_sz-CsAySmAn.svg b/Frontend/Electron/dist-react/assets/flag_sz-CsAySmAn.svg new file mode 100644 index 0000000..cb7f84a --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_sz-CsAySmAn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_ta-Q6DTxsoW.svg b/Frontend/Electron/dist-react/assets/flag_ta-Q6DTxsoW.svg new file mode 100644 index 0000000..547fa05 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_ta-Q6DTxsoW.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_tc-Dn_lC0KY.svg b/Frontend/Electron/dist-react/assets/flag_tc-Dn_lC0KY.svg new file mode 100644 index 0000000..3c61bc7 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_tc-Dn_lC0KY.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_tm-_4vioey7.svg b/Frontend/Electron/dist-react/assets/flag_tm-_4vioey7.svg new file mode 100644 index 0000000..a57c35c --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_tm-_4vioey7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_va-BB2uDrB0.svg b/Frontend/Electron/dist-react/assets/flag_va-BB2uDrB0.svg new file mode 100644 index 0000000..7b2bffa --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_va-BB2uDrB0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_vg-DWuAWiyw.svg b/Frontend/Electron/dist-react/assets/flag_vg-DWuAWiyw.svg new file mode 100644 index 0000000..d8194cd --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_vg-DWuAWiyw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_vi-vzZjsoBi.svg b/Frontend/Electron/dist-react/assets/flag_vi-vzZjsoBi.svg new file mode 100644 index 0000000..d0602d2 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_vi-vzZjsoBi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_xk-D1vfCqOd.svg b/Frontend/Electron/dist-react/assets/flag_xk-D1vfCqOd.svg new file mode 100644 index 0000000..39890a9 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_xk-D1vfCqOd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/flag_yt-BfOxXbO5.svg b/Frontend/Electron/dist-react/assets/flag_yt-BfOxXbO5.svg new file mode 100644 index 0000000..76765b9 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/flag_yt-BfOxXbO5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/gg sans Bold-BGlwbW8t.woff b/Frontend/Electron/dist-react/assets/gg sans Bold-BGlwbW8t.woff new file mode 100644 index 0000000..ced66ca Binary files /dev/null and b/Frontend/Electron/dist-react/assets/gg sans Bold-BGlwbW8t.woff differ diff --git a/Frontend/Electron/dist-react/assets/gg sans Medium-BMWm4JFW.woff b/Frontend/Electron/dist-react/assets/gg sans Medium-BMWm4JFW.woff new file mode 100644 index 0000000..68000a2 Binary files /dev/null and b/Frontend/Electron/dist-react/assets/gg sans Medium-BMWm4JFW.woff differ diff --git a/Frontend/Electron/dist-react/assets/gg sans Regular-Bd8GJPVd.woff b/Frontend/Electron/dist-react/assets/gg sans Regular-Bd8GJPVd.woff new file mode 100644 index 0000000..17cb83f Binary files /dev/null and b/Frontend/Electron/dist-react/assets/gg sans Regular-Bd8GJPVd.woff differ diff --git a/Frontend/Electron/dist-react/assets/gg sans Semibold-xAGa8zYH.woff b/Frontend/Electron/dist-react/assets/gg sans Semibold-xAGa8zYH.woff new file mode 100644 index 0000000..44a64cd Binary files /dev/null and b/Frontend/Electron/dist-react/assets/gg sans Semibold-xAGa8zYH.woff differ diff --git a/Frontend/Electron/dist-react/assets/gloves-BcY_RgAR.svg b/Frontend/Electron/dist-react/assets/gloves-BcY_RgAR.svg new file mode 100644 index 0000000..1b028b2 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/gloves-BcY_RgAR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/hedgehog-CMNxZzfp.svg b/Frontend/Electron/dist-react/assets/hedgehog-CMNxZzfp.svg new file mode 100644 index 0000000..ebbfc2a --- /dev/null +++ b/Frontend/Electron/dist-react/assets/hedgehog-CMNxZzfp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/hiking_boot-CPXD60gE.svg b/Frontend/Electron/dist-react/assets/hiking_boot-CPXD60gE.svg new file mode 100644 index 0000000..6715028 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/hiking_boot-CPXD60gE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/horse_racing-Cd5KXigQ.svg b/Frontend/Electron/dist-react/assets/horse_racing-Cd5KXigQ.svg new file mode 100644 index 0000000..e4cf152 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/horse_racing-Cd5KXigQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/horse_racing_tone1-BPFu29EM.svg b/Frontend/Electron/dist-react/assets/horse_racing_tone1-BPFu29EM.svg new file mode 100644 index 0000000..a48a00b --- /dev/null +++ b/Frontend/Electron/dist-react/assets/horse_racing_tone1-BPFu29EM.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/horse_racing_tone2-kHM6lt0G.svg b/Frontend/Electron/dist-react/assets/horse_racing_tone2-kHM6lt0G.svg new file mode 100644 index 0000000..f80fc32 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/horse_racing_tone2-kHM6lt0G.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/horse_racing_tone3-1prjoMK9.svg b/Frontend/Electron/dist-react/assets/horse_racing_tone3-1prjoMK9.svg new file mode 100644 index 0000000..0baedbc --- /dev/null +++ b/Frontend/Electron/dist-react/assets/horse_racing_tone3-1prjoMK9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/horse_racing_tone4-DZVx5-VD.svg b/Frontend/Electron/dist-react/assets/horse_racing_tone4-DZVx5-VD.svg new file mode 100644 index 0000000..ef50b0b --- /dev/null +++ b/Frontend/Electron/dist-react/assets/horse_racing_tone4-DZVx5-VD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/horse_racing_tone5-DoKtvypB.svg b/Frontend/Electron/dist-react/assets/horse_racing_tone5-DoKtvypB.svg new file mode 100644 index 0000000..ba0de65 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/horse_racing_tone5-DoKtvypB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Electron/dist-react/assets/index-D1fin5Al.css b/Frontend/Electron/dist-react/assets/index-D1fin5Al.css new file mode 100644 index 0000000..e41f0cc --- /dev/null +++ b/Frontend/Electron/dist-react/assets/index-D1fin5Al.css @@ -0,0 +1 @@ +[data-lk-theme=default]{color-scheme:dark;--lk-bg: #111;--lk-bg2: rgb(29.75, 29.75, 29.75);--lk-bg3: rgb(42.5, 42.5, 42.5);--lk-bg4: rgb(55.25, 55.25, 55.25);--lk-bg5: #444444;--lk-fg: #fff;--lk-fg2: rgb(244.8, 244.8, 244.8);--lk-fg3: rgb(234.6, 234.6, 234.6);--lk-fg4: rgb(224.4, 224.4, 224.4);--lk-fg5: rgb(214.2, 214.2, 214.2);--lk-border-color: rgba(255, 255, 255, .1);--lk-accent-fg: #fff;--lk-accent-bg: #1f8cf9;--lk-accent2: rgb(50.867826087, 150.2, 249.532173913);--lk-accent3: rgb(70.7356521739, 160.4, 250.0643478261);--lk-accent4: rgb(90.6034782609, 170.6, 250.5965217391);--lk-danger-fg: #fff;--lk-danger: #f91f31;--lk-danger2: rgb(249.532173913, 50.867826087, 67.2713043478);--lk-danger3: rgb(250.0643478261, 70.7356521739, 85.5426086957);--lk-danger4: rgb(250.5965217391, 90.6034782609, 103.8139130435);--lk-success-fg: #fff;--lk-success: #1ff968;--lk-success2: rgb(50.867826087, 249.532173913, 117.3930434783);--lk-success3: rgb(70.7356521739, 250.0643478261, 130.7860869565);--lk-success4: rgb(90.6034782609, 250.5965217391, 144.1791304348);--lk-control-fg: var(--lk-fg);--lk-control-bg: var(--lk-bg2);--lk-control-hover-bg: var(--lk-bg3);--lk-control-active-bg: var(--lk-bg4);--lk-control-active-hover-bg: var(--lk-bg5);--lk-connection-excellent: #06db4d;--lk-connection-good: #f9b11f;--lk-connection-poor: #f91f31;--lk-font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";--lk-font-size: 16px;--lk-line-height: 1.5;--lk-border-radius: .5rem;--lk-box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .15);--lk-drop-shadow: rgba(255, 255, 255, .2) 0px 0px 24px;--lk-grid-gap: .5rem;--lk-control-bar-height: 69px;--lk-chat-header-height: 69px}.lk-button,.lk-start-audio-button,.lk-chat-toggle,.lk-disconnect-button{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1rem;color:var(--lk-control-fg);background-image:none;background-color:var(--lk-control-bg);border:0;border-radius:var(--lk-border-radius);cursor:pointer;white-space:nowrap;font-size:inherit;line-height:inherit;-webkit-user-select:none;user-select:none}.lk-button:not(:disabled):hover,.lk-start-audio-button:not(:disabled):hover,.lk-chat-toggle:not(:disabled):hover,.lk-disconnect-button:not(:disabled):hover{background-color:var(--lk-control-hover-bg)}.lk-button>svg,.lk-start-audio-button>svg,.lk-chat-toggle>svg,.lk-disconnect-button>svg{overflow:visible}.lk-button[aria-pressed=true],[aria-pressed=true].lk-start-audio-button,[aria-pressed=true].lk-chat-toggle,[aria-pressed=true].lk-disconnect-button{background-color:var(--lk-control-active-bg)}.lk-button[aria-pressed=true]:hover,[aria-pressed=true].lk-start-audio-button:hover,[aria-pressed=true].lk-chat-toggle:hover,[aria-pressed=true].lk-disconnect-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true],[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button{background-color:var(--lk-accent-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true]:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button:hover{background-color:var(--lk-accent2)}.lk-button:disabled,.lk-start-audio-button:disabled,.lk-chat-toggle:disabled,.lk-disconnect-button:disabled{opacity:.5}.lk-button-group{display:inline-flex;align-items:stretch;height:100%}.lk-button-group>.lk-button:first-child,.lk-button-group>.lk-start-audio-button:first-child,.lk-button-group>.lk-chat-toggle:first-child,.lk-button-group>.lk-disconnect-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-button-group-menu{position:relative;flex-shrink:0}.lk-button-group-menu>.lk-button,.lk-button-group-menu>.lk-start-audio-button,.lk-button-group-menu>.lk-chat-toggle,.lk-button-group-menu>.lk-disconnect-button{height:100%;border-top-left-radius:0;border-bottom-left-radius:0}.lk-button-group-menu>.lk-button:after,.lk-button-group-menu>.lk-start-audio-button:after,.lk-button-group-menu>.lk-chat-toggle:after,.lk-button-group-menu>.lk-disconnect-button:after{margin-left:0}.lk-button-menu:after{display:inline-block;content:"";width:.5em;height:.5em;margin-top:-.25rem;margin-left:.5rem;border-left:.125em solid;border-bottom:.125em solid;transform:rotate(-45deg);transform-origin:center center}.lk-disconnect-button{font-weight:600;color:var(--lk-danger);border:1px solid var(--lk-danger)}.lk-disconnect-button:not(:disabled):hover{--lk-control-hover-bg: var(--lk-danger2);color:var(--lk-danger-fg)}.lk-disconnect-button:not(:disabled):active{--lk-control-hover-bg: var(--lk-danger3);color:var(--lk-danger-fg)}.lk-chat-toggle{position:relative}.lk-chat-toggle[data-lk-unread-msgs]:not([data-lk-unread-msgs="0"]):after{content:attr(data-lk-unread-msgs);position:absolute;top:0;left:0;padding:.25rem;margin-left:.25rem;margin-top:.25rem;border-radius:50%;font-size:.5rem;line-height:.75;background:var(--lk-accent-bg)}.lk-media-device-select:not(:last-child){padding-bottom:.5rem;margin-bottom:.75rem;border-bottom:1px solid var(--lk-border-color)}.lk-media-device-select li:not(:last-child){margin-bottom:.25rem}.lk-media-device-select li>.lk-button{width:100%;justify-content:start;padding-block:.5rem}.lk-media-device-select li:not([data-lk-active=true])>.lk-button:not(:disabled):hover{background-color:var(--lk-bg3)}.lk-media-device-select [data-lk-active=false]>.lk-button:hover{cursor:pointer;background-color:#0000000d}.lk-media-device-select [data-lk-active=true]>.lk-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}.lk-device-menu{width:max-content;position:absolute;top:0;left:0;z-index:5;min-width:10rem;padding:.5rem;margin-bottom:.25rem;white-space:nowrap;background-color:var(--lk-bg2);border:1px solid var(--lk-border-color);border-radius:.75rem;box-shadow:var(--lk-box-shadow)}.lk-device-menu-heading{padding:.25rem .5rem;font-weight:700;opacity:.65}.lk-start-audio-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}@media screen and (max-width:600px){.lk-start-audio-button{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}}.lk-pagination-control{position:absolute;bottom:1rem;left:50%;transform:translate(-50%);display:flex;align-items:stretch;background-color:var(--lk-control-bg);border-radius:var(--lk-border-radius);transition:opacity ease-in-out .15s;opacity:0}.lk-pagination-control:hover{opacity:1}.lk-pagination-control>.lk-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-pagination-control>.lk-button:first-child>svg{transform:rotate(180deg)}.lk-pagination-control>.lk-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.lk-pagination-count{padding:.5rem .875rem;border-inline:1px solid var(--lk-bg)}[data-lk-user-interaction=true].lk-pagination-control{opacity:1}.lk-pagination-indicator{position:absolute;height:var(--lk-grid-gap);background-color:var(--lk-bg2);width:fit-content;padding:.2rem .5rem;bottom:calc(var(--lk-grid-gap)/2);left:50%;transform:translate(-50%);border-radius:2rem;opacity:1;display:flex;gap:.2rem;align-items:center}.lk-pagination-indicator span{display:inline-block;width:.4rem;height:.4rem;border-radius:9999999px;background-color:var(--lk-fg);opacity:.35;transition:opacity linear .2s}.lk-pagination-indicator span[data-lk-active]{opacity:.9}.lk-grid-layout{--lk-col-count: 1;--lk-row-count: 1;display:grid;grid-template-columns:repeat(var(--lk-col-count),minmax(0,1fr));grid-auto-rows:minmax(0,1fr);grid-gap:var(--lk-grid-gap);width:100%;height:100%;max-width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-grid-layout[data-lk-pagination=true]{padding-bottom:calc(var(--lk-grid-gap)*2)}.lk-focus-layout{display:grid;grid-template-columns:1fr 5fr;gap:var(--lk-grid-gap);width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-focused-participant{position:relative}.lk-focused-participant .lk-pip-track{position:absolute;top:10px;right:10px;width:20%;height:auto}@media(max-width:600px){.lk-focus-layout{grid-template-columns:1fr;grid-template-rows:5fr 1fr}.lk-carousel{order:1}}.lk-carousel{max-height:100%;display:flex;gap:var(--lk-grid-gap)}.lk-carousel>*{flex-shrink:0;aspect-ratio:16/10;scroll-snap-align:start}.lk-carousel[data-lk-orientation=vertical]{flex-direction:column;scroll-snap-type:y mandatory;overflow-y:auto;overflow-x:hidden}.lk-carousel[data-lk-orientation=vertical]>*{--lk-height-minus-gaps: calc(100% - calc(var(--lk-grid-gap) * calc(var(--lk-max-visible-tiles) - 1)));height:calc(var(--lk-height-minus-gaps)/var(--lk-max-visible-tiles))}.lk-carousel[data-lk-orientation=horizontal]{scroll-snap-type:x mandatory;overflow-y:hidden;overflow-x:auto}.lk-carousel[data-lk-orientation=horizontal]>*{--lk-width-minus-gaps: calc(100% - var(--lk-grid-gap) * (var(--lk-max-visible-tiles) - 1));width:calc(var(--lk-width-minus-gaps)/var(--lk-max-visible-tiles))}.lk-connection-quality{width:1.5rem;height:1.5rem}.lk-track-muted-indicator-camera,.lk-track-muted-indicator-microphone{position:relative;width:var(--lk-indicator-size, 1rem);height:var(--lk-indicator-size, 1rem);margin-inline-end:.25rem;transition:opacity .25s ease-in-out}.lk-track-muted-indicator-camera[data-lk-muted=true]{opacity:.5}.lk-track-muted-indicator-microphone{--lk-bg: var(--lk-icon-mic)}.lk-track-muted-indicator-microphone[data-lk-muted=true]{opacity:.5}.lk-participant-name{font-size:.875rem}.lk-participant-media-video{width:100%;height:100%;object-fit:cover;object-position:center;background-color:#000}.lk-participant-media-video[data-lk-orientation=landscape]{object-fit:cover}.lk-participant-media-video[data-lk-orientation=portrait],.lk-participant-media-video[data-lk-source=screen_share]{object-fit:contain;background-color:var(--lk-bg2)}.lk-participant-media-audio{width:auto}[data-lk-facing-mode=user] .lk-participant-media-video[data-lk-local-participant=true][data-lk-source=camera]{transform:rotateY(180deg)}.lk-audio-visualizer{width:100%;height:100%;min-height:160px;background:var(--lk-bg-control);aspect-ratio:16/9;border-radius:.5rem;display:flex;justify-content:space-around;align-items:center}.lk-audio-visualizer>rect{fill:var(--lk-accent-bg);transition:transform .1s cubic-bezier(.19,.02,.09,1)}.lk-audio-visualizer>path{stroke:var(--lk-accent-bg);transition:.1s cubic-bezier(.19,.02,.09,1)}.lk-audio-bar-visualizer{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--lk-bg);gap:var(--lk-va-bar-gap, 24px)}.lk-audio-bar-visualizer>.lk-audio-bar{transform-origin:"center";height:100%;width:var(--lk-va-bar-width, 12px);border-radius:var(--lk-va-bar-border-radius, 32px);background-color:var(--lk-va-bar-bg, rgba(136, 136, 136, .2));transition:background-color .25s ease-out}.lk-audio-bar-visualizer[data-lk-va-state=speaking]>.lk-audio-bar,.lk-audio-bar-visualizer>.lk-audio-bar.lk-highlighted,.lk-audio-bar-visualizer>[data-lk-highlighted=true]{background-color:var(--lk-fg, rgb(136, 136, 136));transition:none}.lk-audio-bar-visualizer[data-lk-va-state=thinking]{transition:background-color .15s ease-out}.lk-participant-tile{--lk-speaking-indicator-width: 2.5px;position:relative;display:flex;flex-direction:column;gap:.375rem;overflow:hidden;border-radius:var(--lk-border-radius)}.lk-participant-tile:after{content:"";position:absolute;inset:0;border-radius:var(--lk-border-radius);border:0px solid var(--lk-accent-bg);transition-property:border opacity;transition-delay:.5s;transition-duration:.4s;pointer-events:none}.lk-participant-tile[data-lk-speaking=true]:not([data-lk-source=screen_share]):after{transition-delay:0s;transition-duration:.2s;border-width:var(--lk-speaking-indicator-width)}.lk-participant-tile .lk-focus-toggle-button{position:absolute;top:.25rem;right:.25rem;padding:.25rem;background-color:#00000080;border-radius:calc(var(--lk-border-radius)/2);opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile:hover .lk-focus-toggle-button,.lk-participant-tile:focus .lk-focus-toggle-button{opacity:1;transition-delay:0}.lk-participant-tile .lk-connection-quality{opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile .lk-connection-quality[data-lk-quality=poor]{opacity:1;transition-delay:0}.lk-participant-tile:hover .lk-connection-quality,.lk-participant-tile:focus .lk-connection-quality{opacity:1;transition-delay:0}.lk-participant-tile .lk-participant-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:var(--lk-bg2);opacity:0;transition:opacity .2s ease-in-out;pointer-events:none;border-radius:var(--lk-border-radius)}.lk-participant-tile .lk-participant-placeholder svg{height:100%;width:auto;padding:10%}.lk-participant-tile[data-lk-video-muted=true][data-lk-source=camera] .lk-participant-placeholder{opacity:1}.lk-participant-metadata{position:absolute;right:.25rem;bottom:.25rem;left:.25rem;display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:.5rem;line-height:1}.lk-participant-metadata-item{display:flex;align-items:center;padding:.25rem;background-color:#00000080;border-radius:calc(var(--lk-border-radius)/2)}.lk-toast{position:fixed;top:.75rem;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow)}.lk-spinner{animation:lk-rotate 2s infinite linear}@keyframes lk-rotate{0%{transform:rotate(0)}to{transform:rotate(359deg)}}.lk-room-container{background-color:var(--lk-bg);line-height:var(--lk-line-height)}.lk-room-container{position:relative;width:100%;height:100%;--lk-has-imported-styles: "true"}.lk-room-container *[class^=lk-],.lk-room-container *[class*=" lk-"]{box-sizing:border-box}.lk-audio-conference{position:relative;width:100%;height:100%}.lk-audio-conference-stage{width:100%;height:100%;display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.lk-chat{display:grid;grid-template-rows:var(--lk-chat-header-height) 1fr var(--lk-control-bar-height);width:clamp(200px,55ch,60ch);background-color:var(--lk-bg2);border-left:1px solid var(--lk-border-color);align-items:end}.lk-chat-header{height:var(--lk-chat-header-height);padding:.75rem;position:relative;display:flex;align-items:center;justify-content:center}.lk-chat-header .lk-close-button{position:absolute;right:0;transform:translate(-50%);background-color:#0000}.lk-chat-header .lk-close-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-chat-messages{display:flex;width:100%;max-height:100%;flex-direction:column;gap:.25rem;overflow:auto}.lk-chat-entry{display:flex;flex-direction:column;gap:.25rem;margin:0 .25rem}.lk-chat-entry .lk-meta-data{font-size:.75rem;color:var(--lk-fg5);white-space:nowrap;padding:0 .3rem;display:flex}.lk-chat-entry .lk-meta-data .lk-participant-name{margin-top:1rem}.lk-chat-entry .lk-meta-data .lk-timestamp{margin-left:auto;align-self:flex-end}.lk-chat-entry .lk-edit-button{background:none;float:right;margin:0;padding:0 .25rem;border-radius:0;font-size:12px}.lk-chat-entry .lk-message-body{display:inline-block;border-radius:15px;padding:.25rem .75rem;word-break:break-word;width:fit-content;max-width:calc(100% - 32px)}.lk-chat-entry[data-lk-message-origin=local] .lk-message-body{background-color:var(--lk-bg5)}.lk-chat-entry[data-lk-message-origin=remote] .lk-message-body{background-color:var(--lk-accent4)}.lk-chat-entry a{text-decoration:underline;color:inherit}.lk-chat-entry *{margin-block-start:.25em;margin-block-end:.25em}.lk-chat-entry:last-child{margin-bottom:.25rem}.lk-chat-form{display:flex;gap:.75rem;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-chat-form-input{font-size:inherit;line-height:inherit;width:100%}@media(max-width:600px){.lk-chat{position:fixed;top:0;right:0;max-width:100%;bottom:var(--lk-control-bar-height)}}.lk-control-bar,.lk-agent-control-bar{display:flex;gap:.5rem;align-items:center;justify-content:center;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-agent-control-bar{height:var(--lk-control-bar-height);--lk-bg: transparent;--lk-va-bar-width: 2px;--lk-va-bar-gap: 4px;--lk-va-bar-border-radius: 1px}.lk-agent-control-bar .lk-audio-bar-visualizer .lk-audio-bar.lk-highlighted{filter:none}.lk-prejoin{background-color:var(--lk-bg);line-height:var(--lk-line-height)}[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}.lk-prejoin{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;padding:1rem;gap:1rem;margin-inline:auto;background-color:var(--lk-bg);width:min(100%,480px);align-items:stretch}.lk-prejoin .lk-video-container{position:relative;width:100%;height:auto;aspect-ratio:16/10;background-color:#000;border-radius:var(--lk-border-radius);overflow:hidden}.lk-prejoin .lk-video-container video,.lk-prejoin .lk-video-container .lk-camera-off-note{display:block;width:100%;height:100%;object-fit:cover}.lk-prejoin .lk-video-container video[data-lk-facing-mode=user]{transform:rotateY(180deg)}.lk-prejoin .lk-video-container .lk-camera-off-note{position:absolute;top:0;left:0;width:100%;aspect-ratio:16/10;background-color:#000;display:grid;place-items:center}.lk-prejoin .lk-video-container .lk-camera-off-note>*{height:70%;max-width:100%}.lk-prejoin .lk-audio-container{display:none}.lk-prejoin .lk-audio-container audio{width:100%;height:auto}.lk-prejoin .lk-button-group-container{display:flex;flex-wrap:nowrap;gap:1rem}.lk-prejoin .lk-button-group-container>.lk-button-group{width:50%}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button{justify-content:left}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button:first-child{width:100%}@media(max-width:400px){.lk-prejoin .lk-button-group-container{flex-wrap:wrap}.lk-prejoin .lk-button-group-container>.lk-button-group{width:100%}}.lk-prejoin .lk-username-container{display:flex;flex-direction:column;gap:1rem;width:100%;max-width:100%}.lk-prejoin .lk-join-button{--lk-control-fg: var(--lk-accent-fg);--lk-control-bg: var(--lk-accent-bg);--lk-control-hover-bg: var(--lk-accent2);--lk-control-active-bg: var(--lk-accent3);--lk-control-active-hover-bg: var(--lk-accent4);background-color:var(--lk-control-bg)}.lk-prejoin .lk-join-button:hover{background-color:var(--lk-control-hover-bg)}.lk-focus-layout-wrapper,.lk-grid-layout-wrapper{position:relative;display:flex;justify-content:center;width:100%;height:calc(100% - var(--lk-control-bar-height))}.lk-grid-layout-wrapper{flex-direction:column;align-items:center}.lk-focus-layout-wrapper{align-items:stretch}.lk-video-conference{position:relative;display:flex;align-items:stretch;height:100%}.lk-video-conference-inner{display:flex;flex-direction:column;align-items:stretch;width:100%}.lk-settings-menu-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--lk-bg);padding:1rem;display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow);min-width:50vw;min-height:50vh;max-width:100%;max-height:100%;overflow-y:auto}@font-face{font-family:gg sans;src:local("gg sans"),url(./gg%20sans%20Regular-Bd8GJPVd.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:gg sans;src:local("gg sans Medium"),url(./gg%20sans%20Medium-BMWm4JFW.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:gg sans;src:local("gg sans Semibold"),url(./gg%20sans%20Semibold-xAGa8zYH.woff) format("woff");font-weight:600;font-style:normal}@font-face{font-family:gg sans;src:local("gg sans Bold"),url(./gg%20sans%20Bold-BGlwbW8t.woff) format("woff");font-weight:700;font-style:normal}:root{--bg-primary: #1a1a1e;--bg-secondary: hsl(240 5.263% 7.451% /1);--bg-tertiary: #121214;--div-border: #222225;--text-normal: #dcddde;--text-muted: #72767d;--header-primary: #ffffff;--header-secondary: #b9bbbe;--interactive-normal: #b9bbbe;--interactive-hover: #dcddde;--interactive-active: #ffffff;--brand-experiment: #5865f2;--brand-experiment-hover: #4752c4;--input-background: #202225;--danger: #ed4245;--text-default: color-mix(in oklab, hsl(240 3.226% 93.922% /1) 100%, #000 0%)}body{margin:0;padding:0;font-family:gg sans,Noto Sans,Helvetica Neue,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-normal);-webkit-font-smoothing:antialiased;overflow:hidden}.auth-container{display:flex;align-items:center;justify-content:center;height:100vh;background-image:url(https://discord.com/assets/f9e794909795f472.svg);background-size:cover;background-position:center}.auth-box{background-color:var(--bg-secondary);padding:32px;border-radius:5px;width:480px;box-shadow:0 2px 10px #0003}.auth-header{text-align:center;margin-bottom:20px}.auth-header h2{color:var(--header-primary);font-size:24px;font-weight:600;margin-bottom:8px}.auth-header p{color:var(--header-secondary);font-size:16px}.form-group{margin-bottom:20px}.form-group label{display:block;color:var(--header-secondary);font-size:12px;font-weight:700;text-transform:uppercase;margin-bottom:8px}.form-group input{width:100%;padding:10px;background-color:var(--input-background);border:1px solid rgba(0,0,0,.3);border-radius:3px;color:var(--text-normal);font-size:16px;box-sizing:border-box;transition:border-color .2s}.form-group input:focus{border-color:var(--brand-experiment);outline:none}.auth-button{width:100%;padding:12px;background-color:var(--brand-experiment);color:#fff;border:none;border-radius:3px;font-size:16px;font-weight:500;cursor:pointer;transition:background-color .2s}.auth-button:hover{background-color:var(--brand-experiment-hover)}.auth-footer{margin-top:16px;font-size:14px;color:var(--text-muted)}.auth-footer a{color:var(--brand-experiment);text-decoration:none}.sidebar{width:300px;min-width:300px;background-color:var(--bg-secondary);display:flex;flex-direction:row;flex-shrink:0}.server-list{width:72px;border-right:1px solid var(--div-border);background-color:var(--bg-tertiary);display:flex;flex-direction:column;align-items:center;padding-top:12px;flex-shrink:0}.server-icon{width:48px;height:48px;background-color:var(--bg-primary);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--text-normal);cursor:pointer;transition:border-radius .2s,background-color .2s}.server-icon:hover,.server-icon.active{border-radius:30%;background-color:var(--brand-experiment);color:#fff}.channel-list{flex:1;background-color:var(--bg-secondary);padding:10px}.channel-header{padding:0 8px;margin-bottom:16px;font-weight:700;color:var(--header-primary);text-transform:uppercase;font-size:12px}.channel-item{padding:8px;margin-bottom:2px;border-radius:4px;color:color-mix(in oklab,hsl(0 0% 98.431% /1) 100%,#000 0%);font-weight:500;cursor:pointer}.channel-item:hover{background-color:var(--bg-tertiary);color:var(--interactive-hover)}.channel-item.active{background-color:#4f545c52;color:var(--interactive-active)}.app-container{display:flex;height:100vh;width:100vw;overflow:hidden}.chat-area{flex:1;background-color:#1a1a1e;display:flex;flex-direction:column;position:relative;min-width:0}.messages-list{flex:1;overflow-y:auto;padding:0 0 20px;display:flex;flex-direction:column}.messages-list::-webkit-scrollbar{width:8px;background-color:#2b2d31}.messages-list::-webkit-scrollbar-thumb{background-color:#1a1b1e;border-radius:4px}.message-item{display:flex;padding:2px 16px;margin-top:17px}.message-item:hover{background-color:#0202020f}.message-avatar-wrapper{width:40px;margin-right:16px;margin-top:2px}.message-avatar{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:500;color:#fff;font-size:18px;-webkit-user-select:none;user-select:none}.message-body{flex:1;min-width:0}.message-header{display:flex;align-items:center;margin-bottom:2px}.username{font-size:1rem;font-weight:500;margin-right:.25rem;cursor:pointer}.username:hover{text-decoration:underline}.timestamp{color:var(--text-muted);font-size:.75rem;margin-left:.25rem;font-weight:400}.message-content{color:var(--text-normal);font-size:1rem;line-height:1.375rem;white-space:pre-wrap;word-wrap:break-word}.message-content strong{font-weight:700}.message-content h1,.message-content h2,.message-content h3{border-bottom:none;font-weight:700;color:var(--header-primary)}.message-content ul,.message-content ol{list-style-type:disc}.message-content a{color:#00b0f4;text-decoration:none}.message-content a:hover{text-decoration:underline}.message-content blockquote{border-left:4px solid #4f545c;margin:4px 0;padding:0 8px 0 12px;color:#b9bbbe;background-color:transparent}.message-content code{background-color:#2f3136;padding:2px 4px;border-radius:3px;font-family:Consolas,Courier New,monospace;font-size:.875rem}.message-content pre{background-color:#2f3136!important;border-radius:4px;border:1px solid #202225;padding:8px!important;margin:8px 0!important;max-width:100%;overflow-x:auto}.chat-input-form{padding:0 8px 8px;margin-top:8px;background-color:var(--bg-primary)}.chat-input-wrapper{background-color:color-mix(in oklab,hsl(240 5.882% 13.333% /1) 100%,#000 0%);border-radius:8px;display:flex;align-items:center;padding:0 16px;min-height:56px}.chat-input-file-btn{background:none;border:none;color:#b9bbbe;cursor:pointer;padding:0 16px 0 0;display:flex;align-items:center;height:24px;align-self:center}.chat-input-file-btn:hover{color:var(--text-normal)}.chat-input-form textarea{flex:1;padding:11px 0;background-color:transparent;border:none;color:var(--text-default);font-size:16px;font-family:inherit;height:44px;resize:none;overflow:hidden;line-height:1.375rem;box-sizing:border-box}.chat-input-form textarea:focus{outline:none}.chat-input-form textarea::placeholder{color:#72767d}.chat-input-icons{display:flex;align-items:center;padding:0 0 0 10px;align-self:center;height:100%}.chat-input-icon-btn{background:none;border:none;color:#b9bbbe;cursor:pointer;padding:4px;margin-left:4px;display:flex;align-items:center}.chat-input-icon-btn:hover{color:var(--text-normal)}.link-preview{display:flex;background-color:#2f3136;border-left:4px solid #202225;border-radius:4px;padding:10px;margin-top:8px;max-width:520px;width:100%;gap:16px;overflow:hidden;box-sizing:border-box}.preview-content{flex:1;display:flex;flex-direction:column;justify-content:center;min-width:0}.preview-site-name{font-size:12px;color:#b9bbbe;margin-bottom:4px}.preview-title{font-size:16px;font-weight:600;color:#00b0f4;text-decoration:none;margin-bottom:4px;display:block;word-wrap:break-word}.preview-title:hover{text-decoration:underline}.messages-content-wrapper{margin-top:auto;display:flex;flex-direction:column}.preview-description{font-size:14px;color:#dcddde;line-height:1.4;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.preview-image-container{flex-shrink:0;position:relative}.preview-image{max-width:150px;max-height:150px;border-radius:4px;object-fit:cover}.play-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#0009;border-radius:50%;width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:20px}.youtube-preview{flex-direction:column;align-items:flex-start;gap:8px}.youtube-preview .preview-image-container{width:100%;max-width:400px;position:relative}.youtube-preview .preview-image{width:100%;max-width:100%;max-height:none;aspect-ratio:16 / 9}.youtube-video-wrapper{margin-top:8px;position:relative;width:100%;max-width:560px;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:4px;background-color:#000}.youtube-video-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0}.date-divider{display:flex;align-items:center;justify-content:center;margin:24px 16px 8px;position:relative}.date-divider:before{content:"";position:absolute;top:50%;left:0;right:0;height:1px;background-color:#42454a;z-index:1}.date-divider span{background-color:var(--bg-primary);padding:0 8px;color:var(--text-muted);font-size:12px;font-weight:600;position:relative;z-index:2}.verification-failed{display:flex;align-items:center;margin-right:.25rem;cursor:help}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.no-scrollbar::-webkit-scrollbar{display:none} diff --git a/Frontend/Electron/dist-react/assets/index-UkvvH8Ct.js b/Frontend/Electron/dist-react/assets/index-UkvvH8Ct.js new file mode 100644 index 0000000..100e920 --- /dev/null +++ b/Frontend/Electron/dist-react/assets/index-UkvvH8Ct.js @@ -0,0 +1,96 @@ +(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))l(a);new MutationObserver(a=>{for(const n of a)if(n.type==="childList")for(const r of n.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&l(r)}).observe(document,{childList:!0,subtree:!0});function t(a){const n={};return a.integrity&&(n.integrity=a.integrity),a.referrerPolicy&&(n.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?n.credentials="include":a.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function l(a){if(a.ep)return;a.ep=!0;const n=t(a);fetch(a.href,n)}})();function gc(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var oi={exports:{}},Y7={};var ou;function BE(){if(ou)return Y7;ou=1;var e=Symbol.for("react.transitional.element"),c=Symbol.for("react.fragment");function t(l,a,n){var r=null;if(n!==void 0&&(r=""+n),a.key!==void 0&&(r=""+a.key),"key"in a){n={};for(var o in a)o!=="key"&&(n[o]=a[o])}else n=a;return a=n.ref,{$$typeof:e,type:l,key:r,ref:a!==void 0?a:null,props:n}}return Y7.Fragment=c,Y7.jsx=t,Y7.jsxs=t,Y7}var hu;function kE(){return hu||(hu=1,oi.exports=BE()),oi.exports}var D=kE(),hi={exports:{}},B2={};var du;function LE(){if(du)return B2;du=1;var e=Symbol.for("react.transitional.element"),c=Symbol.for("react.portal"),t=Symbol.for("react.fragment"),l=Symbol.for("react.strict_mode"),a=Symbol.for("react.profiler"),n=Symbol.for("react.consumer"),r=Symbol.for("react.context"),o=Symbol.for("react.forward_ref"),d=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),f=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),_=Symbol.iterator;function z(O){return O===null||typeof O!="object"?null:(O=_&&O[_]||O["@@iterator"],typeof O=="function"?O:null)}var M={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,x={};function F(O,X,T){this.props=O,this.context=X,this.refs=x,this.updater=T||M}F.prototype.isReactComponent={},F.prototype.setState=function(O,X){if(typeof O!="object"&&typeof O!="function"&&O!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,O,X,"setState")},F.prototype.forceUpdate=function(O){this.updater.enqueueForceUpdate(this,O,"forceUpdate")};function L(){}L.prototype=F.prototype;function S(O,X,T){this.props=O,this.context=X,this.refs=x,this.updater=T||M}var b=S.prototype=new L;b.constructor=S,C(b,F.prototype),b.isPureReactComponent=!0;var E=Array.isArray;function y(){}var R={H:null,A:null,T:null,S:null},j=Object.prototype.hasOwnProperty;function I(O,X,T){var m2=T.ref;return{$$typeof:e,type:O,key:X,ref:m2!==void 0?m2:null,props:T}}function B(O,X){return I(O.type,X,O.props)}function P(O){return typeof O=="object"&&O!==null&&O.$$typeof===e}function G(O){var X={"=":"=0",":":"=2"};return"$"+O.replace(/[=:]/g,function(T){return X[T]})}var c2=/\/+/g;function r2(O,X){return typeof O=="object"&&O!==null&&O.key!=null?G(""+O.key):X.toString(36)}function s2(O){switch(O.status){case"fulfilled":return O.value;case"rejected":throw O.reason;default:switch(typeof O.status=="string"?O.then(y,y):(O.status="pending",O.then(function(X){O.status==="pending"&&(O.status="fulfilled",O.value=X)},function(X){O.status==="pending"&&(O.status="rejected",O.reason=X)})),O.status){case"fulfilled":return O.value;case"rejected":throw O.reason}}throw O}function $(O,X,T,m2,F2){var M2=typeof O;(M2==="undefined"||M2==="boolean")&&(O=null);var C2=!1;if(O===null)C2=!0;else switch(M2){case"bigint":case"string":case"number":C2=!0;break;case"object":switch(O.$$typeof){case e:case c:C2=!0;break;case f:return C2=O._init,$(C2(O._payload),X,T,m2,F2)}}if(C2)return F2=F2(O),C2=m2===""?"."+r2(O,0):m2,E(F2)?(T="",C2!=null&&(T=C2.replace(c2,"$&/")+"/"),$(F2,X,T,"",function(w2){return w2})):F2!=null&&(P(F2)&&(F2=B(F2,T+(F2.key==null||O&&O.key===F2.key?"":(""+F2.key).replace(c2,"$&/")+"/")+C2)),X.push(F2)),1;C2=0;var k2=m2===""?".":m2+":";if(E(O))for(var l2=0;l2>>1,H=$[f2];if(0>>1;f2a(T,i2))m2a(F2,T)?($[f2]=F2,$[m2]=i2,f2=m2):($[f2]=T,$[X]=i2,f2=X);else if(m2a(F2,i2))$[f2]=F2,$[m2]=i2,f2=m2;else break e}}return e2}function a($,e2){var i2=$.sortIndex-e2.sortIndex;return i2!==0?i2:$.id-e2.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var n=performance;e.unstable_now=function(){return n.now()}}else{var r=Date,o=r.now();e.unstable_now=function(){return r.now()-o}}var d=[],p=[],f=1,g=null,_=3,z=!1,M=!1,C=!1,x=!1,F=typeof setTimeout=="function"?setTimeout:null,L=typeof clearTimeout=="function"?clearTimeout:null,S=typeof setImmediate<"u"?setImmediate:null;function b($){for(var e2=t(p);e2!==null;){if(e2.callback===null)l(p);else if(e2.startTime<=$)l(p),e2.sortIndex=e2.expirationTime,c(d,e2);else break;e2=t(p)}}function E($){if(C=!1,b($),!M)if(t(d)!==null)M=!0,y||(y=!0,G());else{var e2=t(p);e2!==null&&s2(E,e2.startTime-$)}}var y=!1,R=-1,j=5,I=-1;function B(){return x?!0:!(e.unstable_now()-I$&&B());){var f2=g.callback;if(typeof f2=="function"){g.callback=null,_=g.priorityLevel;var H=f2(g.expirationTime<=$);if($=e.unstable_now(),typeof H=="function"){g.callback=H,b($),e2=!0;break c}g===t(d)&&l(d),b($)}else l(d);g=t(d)}if(g!==null)e2=!0;else{var O=t(p);O!==null&&s2(E,O.startTime-$),e2=!1}}break e}finally{g=null,_=i2,z=!1}e2=void 0}}finally{e2?G():y=!1}}}var G;if(typeof S=="function")G=function(){S(P)};else if(typeof MessageChannel<"u"){var c2=new MessageChannel,r2=c2.port2;c2.port1.onmessage=P,G=function(){r2.postMessage(null)}}else G=function(){F(P,0)};function s2($,e2){R=F(function(){$(e.unstable_now())},e2)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function($){$.callback=null},e.unstable_forceFrameRate=function($){0>$||125<$?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):j=0<$?Math.floor(1e3/$):5},e.unstable_getCurrentPriorityLevel=function(){return _},e.unstable_next=function($){switch(_){case 1:case 2:case 3:var e2=3;break;default:e2=_}var i2=_;_=e2;try{return $()}finally{_=i2}},e.unstable_requestPaint=function(){x=!0},e.unstable_runWithPriority=function($,e2){switch($){case 1:case 2:case 3:case 4:case 5:break;default:$=3}var i2=_;_=$;try{return e2()}finally{_=i2}},e.unstable_scheduleCallback=function($,e2,i2){var f2=e.unstable_now();switch(typeof i2=="object"&&i2!==null?(i2=i2.delay,i2=typeof i2=="number"&&0f2?($.sortIndex=i2,c(p,$),t(d)===null&&$===t(p)&&(C?(L(R),R=-1):C=!0,s2(E,i2-f2))):($.sortIndex=H,c(d,$),M||z||(M=!0,y||(y=!0,G()))),$},e.unstable_shouldYield=B,e.unstable_wrapCallback=function($){var e2=_;return function(){var i2=_;_=e2;try{return $.apply(this,arguments)}finally{_=i2}}}})(vi)),vi}var fu;function HE(){return fu||(fu=1,pi.exports=TE()),pi.exports}var fi={exports:{}},i1={};var gu;function RE(){if(gu)return i1;gu=1;var e=as();function c(d){var p="https://react.dev/errors/"+d;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(c){console.error(c)}}return e(),fi.exports=RE(),fi.exports}var mu;function IE(){if(mu)return W7;mu=1;var e=HE(),c=as(),t=VE();function l(i){var s="https://react.dev/errors/"+i;if(1H||(i.current=f2[H],f2[H]=null,H--)}function T(i,s){H++,f2[H]=i.current,i.current=s}var m2=O(null),F2=O(null),M2=O(null),C2=O(null);function k2(i,s){switch(T(M2,s),T(F2,i),T(m2,null),s.nodeType){case 9:case 11:i=(i=s.documentElement)&&(i=i.namespaceURI)?kg(i):0;break;default:if(i=s.tagName,s=s.namespaceURI)s=kg(s),i=Lg(s,i);else switch(i){case"svg":i=1;break;case"math":i=2;break;default:i=0}}X(m2),T(m2,i)}function l2(){X(m2),X(F2),X(M2)}function w2(i){i.memoizedState!==null&&T(C2,i);var s=m2.current,h=Lg(s,i.type);s!==h&&(T(F2,i),T(m2,h))}function L2(i){F2.current===i&&(X(m2),X(F2)),C2.current===i&&(X(C2),G7._currentValue=i2)}var Z2,s0;function N0(i){if(Z2===void 0)try{throw Error()}catch(h){var s=h.stack.trim().match(/\n( *(at )?)/);Z2=s&&s[1]||"",s0=-1)":-1u||U[v]!==W[u]){var a2=` +`+U[v].replace(" at new "," at ");return i.displayName&&a2.includes("")&&(a2=a2.replace("",i.displayName)),a2}while(1<=v&&0<=u);break}}}finally{_1=!1,Error.prepareStackTrace=h}return(h=i?i.displayName||i.name:"")?N0(h):""}function O2(i,s){switch(i.tag){case 26:case 27:case 5:return N0(i.type);case 16:return N0("Lazy");case 13:return i.child!==s&&s!==null?N0("Suspense Fallback"):N0("Suspense");case 19:return N0("SuspenseList");case 0:case 15:return e0(i.type,!1);case 11:return e0(i.type.render,!1);case 1:return e0(i.type,!0);case 31:return N0("Activity");default:return""}}function z1(i){try{var s="",h=null;do s+=O2(i,h),h=i,i=i.return;while(i);return s}catch(v){return` +Error generating stack: `+v.message+` +`+v.stack}}var q1=Object.prototype.hasOwnProperty,h3=e.unstable_scheduleCallback,z4=e.unstable_cancelCallback,Yc=e.unstable_shouldYield,Wc=e.unstable_requestPaint,h1=e.unstable_now,Kc=e.unstable_getCurrentPriorityLevel,o2=e.unstable_ImmediatePriority,_2=e.unstable_UserBlockingPriority,A2=e.unstable_NormalPriority,U2=e.unstable_LowPriority,a0=e.unstable_IdlePriority,L1=e.log,U3=e.unstable_setDisableYieldValue,d1=null,O0=null;function M1(i){if(typeof L1=="function"&&U3(i),O0&&typeof O0.setStrictMode=="function")try{O0.setStrictMode(d1,i)}catch{}}var v0=Math.clz32?Math.clz32:mF,M4=Math.log,d3=Math.LN2;function mF(i){return i>>>=0,i===0?32:31-(M4(i)/d3|0)|0}var K8=256,X8=262144,Q8=4194304;function r5(i){var s=i&42;if(s!==0)return s;switch(i&-i){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return i&261888;case 262144:case 524288:case 1048576:case 2097152:return i&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return i&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return i}}function J8(i,s,h){var v=i.pendingLanes;if(v===0)return 0;var u=0,m=i.suspendedLanes,w=i.pingedLanes;i=i.warmLanes;var A=v&134217727;return A!==0?(v=A&~m,v!==0?u=r5(v):(w&=A,w!==0?u=r5(w):h||(h=A&~i,h!==0&&(u=r5(h))))):(A=v&~m,A!==0?u=r5(A):w!==0?u=r5(w):h||(h=v&~i,h!==0&&(u=r5(h)))),u===0?0:s!==0&&s!==u&&(s&m)===0&&(m=u&-u,h=s&-s,m>=h||m===32&&(h&4194048)!==0)?s:u}function l7(i,s){return(i.pendingLanes&~(i.suspendedLanes&~i.pingedLanes)&s)===0}function _F(i,s){switch(i){case 1:case 2:case 4:case 8:case 64:return s+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return s+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function pp(){var i=Q8;return Q8<<=1,(Q8&62914560)===0&&(Q8=4194304),i}function Xc(i){for(var s=[],h=0;31>h;h++)s.push(i);return s}function i7(i,s){i.pendingLanes|=s,s!==268435456&&(i.suspendedLanes=0,i.pingedLanes=0,i.warmLanes=0)}function zF(i,s,h,v,u,m){var w=i.pendingLanes;i.pendingLanes=h,i.suspendedLanes=0,i.pingedLanes=0,i.warmLanes=0,i.expiredLanes&=h,i.entangledLanes&=h,i.errorRecoveryDisabledLanes&=h,i.shellSuspendCounter=0;var A=i.entanglements,U=i.expirationTimes,W=i.hiddenUpdates;for(h=w&~h;0"u")return null;try{return i.activeElement||i.body}catch{return i.body}}var EF=/[\n"\\]/g;function Z1(i){return i.replace(EF,function(s){return"\\"+s.charCodeAt(0).toString(16)+" "})}function lt(i,s,h,v,u,m,w,A){i.name="",w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"?i.type=w:i.removeAttribute("type"),s!=null?w==="number"?(s===0&&i.value===""||i.value!=s)&&(i.value=""+$1(s)):i.value!==""+$1(s)&&(i.value=""+$1(s)):w!=="submit"&&w!=="reset"||i.removeAttribute("value"),s!=null?it(i,w,$1(s)):h!=null?it(i,w,$1(h)):v!=null&&i.removeAttribute("value"),u==null&&m!=null&&(i.defaultChecked=!!m),u!=null&&(i.checked=u&&typeof u!="function"&&typeof u!="symbol"),A!=null&&typeof A!="function"&&typeof A!="symbol"&&typeof A!="boolean"?i.name=""+$1(A):i.removeAttribute("name")}function Ep(i,s,h,v,u,m,w,A){if(m!=null&&typeof m!="function"&&typeof m!="symbol"&&typeof m!="boolean"&&(i.type=m),s!=null||h!=null){if(!(m!=="submit"&&m!=="reset"||s!=null)){tt(i);return}h=h!=null?""+$1(h):"",s=s!=null?""+$1(s):h,A||s===i.value||(i.value=s),i.defaultValue=s}v=v??u,v=typeof v!="function"&&typeof v!="symbol"&&!!v,i.checked=A?i.checked:!!v,i.defaultChecked=!!v,w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"&&(i.name=w),tt(i)}function it(i,s,h){s==="number"&&t9(i.ownerDocument)===i||i.defaultValue===""+h||(i.defaultValue=""+h)}function q5(i,s,h,v){if(i=i.options,s){s={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ot=!1;if(G3)try{var r7={};Object.defineProperty(r7,"passive",{get:function(){ot=!0}}),window.addEventListener("test",r7,r7),window.removeEventListener("test",r7,r7)}catch{ot=!1}var C4=null,ht=null,i9=null;function kp(){if(i9)return i9;var i,s=ht,h=s.length,v,u="value"in C4?C4.value:C4.textContent,m=u.length;for(i=0;i=d7),Ip=" ",Np=!1;function Op(i,s){switch(i){case"keyup":return JF.indexOf(s.keyCode)!==-1;case"keydown":return s.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Up(i){return i=i.detail,typeof i=="object"&&"data"in i?i.data:null}var W5=!1;function cb(i,s){switch(i){case"compositionend":return Up(s);case"keypress":return s.which!==32?null:(Np=!0,Ip);case"textInput":return i=s.data,i===Ip&&Np?null:i;default:return null}}function tb(i,s){if(W5)return i==="compositionend"||!gt&&Op(i,s)?(i=kp(),i9=ht=C4=null,W5=!1,i):null;switch(i){case"paste":return null;case"keypress":if(!(s.ctrlKey||s.altKey||s.metaKey)||s.ctrlKey&&s.altKey){if(s.char&&1=s)return{node:h,offset:s-i};i=v}e:{for(;h;){if(h.nextSibling){h=h.nextSibling;break e}h=h.parentNode}h=void 0}h=Wp(h)}}function Xp(i,s){return i&&s?i===s?!0:i&&i.nodeType===3?!1:s&&s.nodeType===3?Xp(i,s.parentNode):"contains"in i?i.contains(s):i.compareDocumentPosition?!!(i.compareDocumentPosition(s)&16):!1:!1}function Qp(i){i=i!=null&&i.ownerDocument!=null&&i.ownerDocument.defaultView!=null?i.ownerDocument.defaultView:window;for(var s=t9(i.document);s instanceof i.HTMLIFrameElement;){try{var h=typeof s.contentWindow.location.href=="string"}catch{h=!1}if(h)i=s.contentWindow;else break;s=t9(i.document)}return s}function _t(i){var s=i&&i.nodeName&&i.nodeName.toLowerCase();return s&&(s==="input"&&(i.type==="text"||i.type==="search"||i.type==="tel"||i.type==="url"||i.type==="password")||s==="textarea"||i.contentEditable==="true")}var hb=G3&&"documentMode"in document&&11>=document.documentMode,K5=null,zt=null,g7=null,Mt=!1;function Jp(i,s,h){var v=h.window===h?h.document:h.nodeType===9?h:h.ownerDocument;Mt||K5==null||K5!==t9(v)||(v=K5,"selectionStart"in v&&_t(v)?v={start:v.selectionStart,end:v.selectionEnd}:(v=(v.ownerDocument&&v.ownerDocument.defaultView||window).getSelection(),v={anchorNode:v.anchorNode,anchorOffset:v.anchorOffset,focusNode:v.focusNode,focusOffset:v.focusOffset}),g7&&f7(g7,v)||(g7=v,v=X9(zt,"onSelect"),0>=w,u-=w,E3=1<<32-v0(s)+u|h<H2?(W2=E2,E2=null):W2=E2.sibling;var t0=Q(Z,E2,Y[H2],h2);if(t0===null){E2===null&&(E2=W2);break}i&&E2&&t0.alternate===null&&s(Z,E2),q=m(t0,q,H2),c0===null?x2=t0:c0.sibling=t0,c0=t0,E2=W2}if(H2===Y.length)return h(Z,E2),X2&&$3(Z,H2),x2;if(E2===null){for(;H2H2?(W2=E2,E2=null):W2=E2.sibling;var G4=Q(Z,E2,t0.value,h2);if(G4===null){E2===null&&(E2=W2);break}i&&E2&&G4.alternate===null&&s(Z,E2),q=m(G4,q,H2),c0===null?x2=G4:c0.sibling=G4,c0=G4,E2=W2}if(t0.done)return h(Z,E2),X2&&$3(Z,H2),x2;if(E2===null){for(;!t0.done;H2++,t0=Y.next())t0=d2(Z,t0.value,h2),t0!==null&&(q=m(t0,q,H2),c0===null?x2=t0:c0.sibling=t0,c0=t0);return X2&&$3(Z,H2),x2}for(E2=v(E2);!t0.done;H2++,t0=Y.next())t0=t2(E2,Z,H2,t0.value,h2),t0!==null&&(i&&t0.alternate!==null&&E2.delete(t0.key===null?H2:t0.key),q=m(t0,q,H2),c0===null?x2=t0:c0.sibling=t0,c0=t0);return i&&E2.forEach(function(SE){return s(Z,SE)}),X2&&$3(Z,H2),x2}function d0(Z,q,Y,h2){if(typeof Y=="object"&&Y!==null&&Y.type===C&&Y.key===null&&(Y=Y.props.children),typeof Y=="object"&&Y!==null){switch(Y.$$typeof){case z:e:{for(var x2=Y.key;q!==null;){if(q.key===x2){if(x2=Y.type,x2===C){if(q.tag===7){h(Z,q.sibling),h2=u(q,Y.props.children),h2.return=Z,Z=h2;break e}}else if(q.elementType===x2||typeof x2=="object"&&x2!==null&&x2.$$typeof===j&&z5(x2)===q.type){h(Z,q.sibling),h2=u(q,Y.props),w7(h2,Y),h2.return=Z,Z=h2;break e}h(Z,q);break}else s(Z,q);q=q.sibling}Y.type===C?(h2=f5(Y.props.children,Z.mode,h2,Y.key),h2.return=Z,Z=h2):(h2=f9(Y.type,Y.key,Y.props,null,Z.mode,h2),w7(h2,Y),h2.return=Z,Z=h2)}return w(Z);case M:e:{for(x2=Y.key;q!==null;){if(q.key===x2)if(q.tag===4&&q.stateNode.containerInfo===Y.containerInfo&&q.stateNode.implementation===Y.implementation){h(Z,q.sibling),h2=u(q,Y.children||[]),h2.return=Z,Z=h2;break e}else{h(Z,q);break}else s(Z,q);q=q.sibling}h2=yt(Y,Z.mode,h2),h2.return=Z,Z=h2}return w(Z);case j:return Y=z5(Y),d0(Z,q,Y,h2)}if(s2(Y))return b2(Z,q,Y,h2);if(G(Y)){if(x2=G(Y),typeof x2!="function")throw Error(l(150));return Y=x2.call(Y),D2(Z,q,Y,h2)}if(typeof Y.then=="function")return d0(Z,q,w9(Y),h2);if(Y.$$typeof===S)return d0(Z,q,m9(Z,Y),h2);C9(Z,Y)}return typeof Y=="string"&&Y!==""||typeof Y=="number"||typeof Y=="bigint"?(Y=""+Y,q!==null&&q.tag===6?(h(Z,q.sibling),h2=u(q,Y),h2.return=Z,Z=h2):(h(Z,q),h2=xt(Y,Z.mode,h2),h2.return=Z,Z=h2),w(Z)):h(Z,q)}return function(Z,q,Y,h2){try{M7=0;var x2=d0(Z,q,Y,h2);return n6=null,x2}catch(E2){if(E2===s6||E2===z9)throw E2;var c0=H1(29,E2,null,Z.mode);return c0.lanes=h2,c0.return=Z,c0}}}var w5=Cv(!0),Fv=Cv(!1),y4=!1;function Nt(i){i.updateQueue={baseState:i.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ot(i,s){i=i.updateQueue,s.updateQueue===i&&(s.updateQueue={baseState:i.baseState,firstBaseUpdate:i.firstBaseUpdate,lastBaseUpdate:i.lastBaseUpdate,shared:i.shared,callbacks:null})}function D4(i){return{lane:i,tag:0,payload:null,callback:null,next:null}}function A4(i,s,h){var v=i.updateQueue;if(v===null)return null;if(v=v.shared,(l0&2)!==0){var u=v.pending;return u===null?s.next=s:(s.next=u.next,u.next=s),v.pending=s,s=v9(i),sv(i,null,h),s}return p9(i,v,s,h),v9(i)}function C7(i,s,h){if(s=s.updateQueue,s!==null&&(s=s.shared,(h&4194048)!==0)){var v=s.lanes;v&=i.pendingLanes,h|=v,s.lanes=h,fp(i,h)}}function Ut(i,s){var h=i.updateQueue,v=i.alternate;if(v!==null&&(v=v.updateQueue,h===v)){var u=null,m=null;if(h=h.firstBaseUpdate,h!==null){do{var w={lane:h.lane,tag:h.tag,payload:h.payload,callback:null,next:null};m===null?u=m=w:m=m.next=w,h=h.next}while(h!==null);m===null?u=m=s:m=m.next=s}else u=m=s;h={baseState:v.baseState,firstBaseUpdate:u,lastBaseUpdate:m,shared:v.shared,callbacks:v.callbacks},i.updateQueue=h;return}i=h.lastBaseUpdate,i===null?h.firstBaseUpdate=s:i.next=s,h.lastBaseUpdate=s}var Pt=!1;function F7(){if(Pt){var i=a6;if(i!==null)throw i}}function b7(i,s,h,v){Pt=!1;var u=i.updateQueue;y4=!1;var m=u.firstBaseUpdate,w=u.lastBaseUpdate,A=u.shared.pending;if(A!==null){u.shared.pending=null;var U=A,W=U.next;U.next=null,w===null?m=W:w.next=W,w=U;var a2=i.alternate;a2!==null&&(a2=a2.updateQueue,A=a2.lastBaseUpdate,A!==w&&(A===null?a2.firstBaseUpdate=W:A.next=W,a2.lastBaseUpdate=U))}if(m!==null){var d2=u.baseState;w=0,a2=W=U=null,A=m;do{var Q=A.lane&-536870913,t2=Q!==A.lane;if(t2?(Y2&Q)===Q:(v&Q)===Q){Q!==0&&Q===i6&&(Pt=!0),a2!==null&&(a2=a2.next={lane:0,tag:A.tag,payload:A.payload,callback:null,next:null});e:{var b2=i,D2=A;Q=s;var d0=h;switch(D2.tag){case 1:if(b2=D2.payload,typeof b2=="function"){d2=b2.call(d0,d2,Q);break e}d2=b2;break e;case 3:b2.flags=b2.flags&-65537|128;case 0:if(b2=D2.payload,Q=typeof b2=="function"?b2.call(d0,d2,Q):b2,Q==null)break e;d2=g({},d2,Q);break e;case 2:y4=!0}}Q=A.callback,Q!==null&&(i.flags|=64,t2&&(i.flags|=8192),t2=u.callbacks,t2===null?u.callbacks=[Q]:t2.push(Q))}else t2={lane:Q,tag:A.tag,payload:A.payload,callback:A.callback,next:null},a2===null?(W=a2=t2,U=d2):a2=a2.next=t2,w|=Q;if(A=A.next,A===null){if(A=u.shared.pending,A===null)break;t2=A,A=t2.next,t2.next=null,u.lastBaseUpdate=t2,u.shared.pending=null}}while(!0);a2===null&&(U=d2),u.baseState=U,u.firstBaseUpdate=W,u.lastBaseUpdate=a2,m===null&&(u.shared.lanes=0),T4|=w,i.lanes=w,i.memoizedState=d2}}function bv(i,s){if(typeof i!="function")throw Error(l(191,i));i.call(s)}function Ev(i,s){var h=i.callbacks;if(h!==null)for(i.callbacks=null,i=0;im?m:8;var w=$.T,A={};$.T=A,nl(i,!1,s,h);try{var U=u(),W=$.S;if(W!==null&&W(A,U),U!==null&&typeof U=="object"&&typeof U.then=="function"){var a2=zb(U,v);y7(i,s,a2,O1(i))}else y7(i,s,v,O1(i))}catch(d2){y7(i,s,{then:function(){},status:"rejected",reason:d2},O1())}finally{e2.p=m,w!==null&&A.types!==null&&(w.types=A.types),$.T=w}}function Eb(){}function al(i,s,h,v){if(i.tag!==5)throw Error(l(476));var u=lf(i).queue;tf(i,u,s,i2,h===null?Eb:function(){return af(i),h(v)})}function lf(i){var s=i.memoizedState;if(s!==null)return s;s={memoizedState:i2,baseState:i2,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:K3,lastRenderedState:i2},next:null};var h={};return s.next={memoizedState:h,baseState:h,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:K3,lastRenderedState:h},next:null},i.memoizedState=s,i=i.alternate,i!==null&&(i.memoizedState=s),s}function af(i){var s=lf(i);s.next===null&&(s=i.alternate.memoizedState),y7(i,s.next.queue,{},O1())}function sl(){return Q0(G7)}function sf(){return S0().memoizedState}function nf(){return S0().memoizedState}function xb(i){for(var s=i.return;s!==null;){switch(s.tag){case 24:case 3:var h=O1();i=D4(h);var v=A4(s,i,h);v!==null&&(y1(v,s,h),C7(v,s,h)),s={cache:Ht()},i.payload=s;return}s=s.return}}function yb(i,s,h){var v=O1();h={lane:v,revertLane:0,gesture:null,action:h,hasEagerState:!1,eagerState:null,next:null},k9(i)?of(s,h):(h=bt(i,s,h,v),h!==null&&(y1(h,i,v),hf(h,s,v)))}function rf(i,s,h){var v=O1();y7(i,s,h,v)}function y7(i,s,h,v){var u={lane:v,revertLane:0,gesture:null,action:h,hasEagerState:!1,eagerState:null,next:null};if(k9(i))of(s,u);else{var m=i.alternate;if(i.lanes===0&&(m===null||m.lanes===0)&&(m=s.lastRenderedReducer,m!==null))try{var w=s.lastRenderedState,A=m(w,h);if(u.hasEagerState=!0,u.eagerState=A,T1(A,w))return p9(i,s,u,0),f0===null&&d9(),!1}catch{}if(h=bt(i,s,u,v),h!==null)return y1(h,i,v),hf(h,s,v),!0}return!1}function nl(i,s,h,v){if(v={lane:2,revertLane:Nl(),gesture:null,action:v,hasEagerState:!1,eagerState:null,next:null},k9(i)){if(s)throw Error(l(479))}else s=bt(i,h,v,2),s!==null&&y1(s,i,2)}function k9(i){var s=i.alternate;return i===T2||s!==null&&s===T2}function of(i,s){o6=E9=!0;var h=i.pending;h===null?s.next=s:(s.next=h.next,h.next=s),i.pending=s}function hf(i,s,h){if((h&4194048)!==0){var v=s.lanes;v&=i.pendingLanes,h|=v,s.lanes=h,fp(i,h)}}var D7={readContext:Q0,use:D9,useCallback:b0,useContext:b0,useEffect:b0,useImperativeHandle:b0,useLayoutEffect:b0,useInsertionEffect:b0,useMemo:b0,useReducer:b0,useRef:b0,useState:b0,useDebugValue:b0,useDeferredValue:b0,useTransition:b0,useSyncExternalStore:b0,useId:b0,useHostTransitionStatus:b0,useFormState:b0,useActionState:b0,useOptimistic:b0,useMemoCache:b0,useCacheRefresh:b0};D7.useEffectEvent=b0;var df={readContext:Q0,use:D9,useCallback:function(i,s){return p1().memoizedState=[i,s===void 0?null:s],i},useContext:Q0,useEffect:Zv,useImperativeHandle:function(i,s,h){h=h!=null?h.concat([i]):null,S9(4194308,4,Xv.bind(null,s,i),h)},useLayoutEffect:function(i,s){return S9(4194308,4,i,s)},useInsertionEffect:function(i,s){S9(4,2,i,s)},useMemo:function(i,s){var h=p1();s=s===void 0?null:s;var v=i();if(C5){M1(!0);try{i()}finally{M1(!1)}}return h.memoizedState=[v,s],v},useReducer:function(i,s,h){var v=p1();if(h!==void 0){var u=h(s);if(C5){M1(!0);try{h(s)}finally{M1(!1)}}}else u=s;return v.memoizedState=v.baseState=u,i={pending:null,lanes:0,dispatch:null,lastRenderedReducer:i,lastRenderedState:u},v.queue=i,i=i.dispatch=yb.bind(null,T2,i),[v.memoizedState,i]},useRef:function(i){var s=p1();return i={current:i},s.memoizedState=i},useState:function(i){i=el(i);var s=i.queue,h=rf.bind(null,T2,s);return s.dispatch=h,[i.memoizedState,h]},useDebugValue:ll,useDeferredValue:function(i,s){var h=p1();return il(h,i,s)},useTransition:function(){var i=el(!1);return i=tf.bind(null,T2,i.queue,!0,!1),p1().memoizedState=i,[!1,i]},useSyncExternalStore:function(i,s,h){var v=T2,u=p1();if(X2){if(h===void 0)throw Error(l(407));h=h()}else{if(h=s(),f0===null)throw Error(l(349));(Y2&127)!==0||Bv(v,s,h)}u.memoizedState=h;var m={value:h,getSnapshot:s};return u.queue=m,Zv(Lv.bind(null,v,m,i),[i]),v.flags|=2048,d6(9,{destroy:void 0},kv.bind(null,v,m,h,s),null),h},useId:function(){var i=p1(),s=f0.identifierPrefix;if(X2){var h=x3,v=E3;h=(v&~(1<<32-v0(v)-1)).toString(32)+h,s="_"+s+"R_"+h,h=x9++,0<\/script>",m=m.removeChild(m.firstChild);break;case"select":m=typeof v.is=="string"?w.createElement("select",{is:v.is}):w.createElement("select"),v.multiple?m.multiple=!0:v.size&&(m.size=v.size);break;default:m=typeof v.is=="string"?w.createElement(u,{is:v.is}):w.createElement(u)}}m[K0]=s,m[w1]=v;e:for(w=s.child;w!==null;){if(w.tag===5||w.tag===6)m.appendChild(w.stateNode);else if(w.tag!==4&&w.tag!==27&&w.child!==null){w.child.return=w,w=w.child;continue}if(w===s)break e;for(;w.sibling===null;){if(w.return===null||w.return===s)break e;w=w.return}w.sibling.return=w.return,w=w.sibling}s.stateNode=m;e:switch(e1(m,u,v),u){case"button":case"input":case"select":case"textarea":v=!!v.autoFocus;break e;case"img":v=!0;break e;default:v=!1}v&&Q3(s)}}return z0(s),wl(s,s.type,i===null?null:i.memoizedProps,s.pendingProps,h),null;case 6:if(i&&s.stateNode!=null)i.memoizedProps!==v&&Q3(s);else{if(typeof v!="string"&&s.stateNode===null)throw Error(l(166));if(i=M2.current,t6(s)){if(i=s.stateNode,h=s.memoizedProps,v=null,u=X0,u!==null)switch(u.tag){case 27:case 5:v=u.memoizedProps}i[K0]=s,i=!!(i.nodeValue===h||v!==null&&v.suppressHydrationWarning===!0||Sg(i.nodeValue,h)),i||E4(s,!0)}else i=Q9(i).createTextNode(v),i[K0]=s,s.stateNode=i}return z0(s),null;case 31:if(h=s.memoizedState,i===null||i.memoizedState!==null){if(v=t6(s),h!==null){if(i===null){if(!v)throw Error(l(318));if(i=s.memoizedState,i=i!==null?i.dehydrated:null,!i)throw Error(l(557));i[K0]=s}else g5(),(s.flags&128)===0&&(s.memoizedState=null),s.flags|=4;z0(s),i=!1}else h=Bt(),i!==null&&i.memoizedState!==null&&(i.memoizedState.hydrationErrors=h),i=!0;if(!i)return s.flags&256?(V1(s),s):(V1(s),null);if((s.flags&128)!==0)throw Error(l(558))}return z0(s),null;case 13:if(v=s.memoizedState,i===null||i.memoizedState!==null&&i.memoizedState.dehydrated!==null){if(u=t6(s),v!==null&&v.dehydrated!==null){if(i===null){if(!u)throw Error(l(318));if(u=s.memoizedState,u=u!==null?u.dehydrated:null,!u)throw Error(l(317));u[K0]=s}else g5(),(s.flags&128)===0&&(s.memoizedState=null),s.flags|=4;z0(s),u=!1}else u=Bt(),i!==null&&i.memoizedState!==null&&(i.memoizedState.hydrationErrors=u),u=!0;if(!u)return s.flags&256?(V1(s),s):(V1(s),null)}return V1(s),(s.flags&128)!==0?(s.lanes=h,s):(h=v!==null,i=i!==null&&i.memoizedState!==null,h&&(v=s.child,u=null,v.alternate!==null&&v.alternate.memoizedState!==null&&v.alternate.memoizedState.cachePool!==null&&(u=v.alternate.memoizedState.cachePool.pool),m=null,v.memoizedState!==null&&v.memoizedState.cachePool!==null&&(m=v.memoizedState.cachePool.pool),m!==u&&(v.flags|=2048)),h!==i&&h&&(s.child.flags|=8192),V9(s,s.updateQueue),z0(s),null);case 4:return l2(),i===null&&jl(s.stateNode.containerInfo),z0(s),null;case 10:return Y3(s.type),z0(s),null;case 19:if(X(A0),v=s.memoizedState,v===null)return z0(s),null;if(u=(s.flags&128)!==0,m=v.rendering,m===null)if(u)S7(v,!1);else{if(E0!==0||i!==null&&(i.flags&128)!==0)for(i=s.child;i!==null;){if(m=b9(i),m!==null){for(s.flags|=128,S7(v,!1),i=m.updateQueue,s.updateQueue=i,V9(s,i),s.subtreeFlags=0,i=h,h=s.child;h!==null;)nv(h,i),h=h.sibling;return T(A0,A0.current&1|2),X2&&$3(s,v.treeForkCount),s.child}i=i.sibling}v.tail!==null&&h1()>P9&&(s.flags|=128,u=!0,S7(v,!1),s.lanes=4194304)}else{if(!u)if(i=b9(m),i!==null){if(s.flags|=128,u=!0,i=i.updateQueue,s.updateQueue=i,V9(s,i),S7(v,!0),v.tail===null&&v.tailMode==="hidden"&&!m.alternate&&!X2)return z0(s),null}else 2*h1()-v.renderingStartTime>P9&&h!==536870912&&(s.flags|=128,u=!0,S7(v,!1),s.lanes=4194304);v.isBackwards?(m.sibling=s.child,s.child=m):(i=v.last,i!==null?i.sibling=m:s.child=m,v.last=m)}return v.tail!==null?(i=v.tail,v.rendering=i,v.tail=i.sibling,v.renderingStartTime=h1(),i.sibling=null,h=A0.current,T(A0,u?h&1|2:h&1),X2&&$3(s,v.treeForkCount),i):(z0(s),null);case 22:case 23:return V1(s),Gt(),v=s.memoizedState!==null,i!==null?i.memoizedState!==null!==v&&(s.flags|=8192):v&&(s.flags|=8192),v?(h&536870912)!==0&&(s.flags&128)===0&&(z0(s),s.subtreeFlags&6&&(s.flags|=8192)):z0(s),h=s.updateQueue,h!==null&&V9(s,h.retryQueue),h=null,i!==null&&i.memoizedState!==null&&i.memoizedState.cachePool!==null&&(h=i.memoizedState.cachePool.pool),v=null,s.memoizedState!==null&&s.memoizedState.cachePool!==null&&(v=s.memoizedState.cachePool.pool),v!==h&&(s.flags|=2048),i!==null&&X(_5),null;case 24:return h=null,i!==null&&(h=i.memoizedState.cache),s.memoizedState.cache!==h&&(s.flags|=2048),Y3(k0),z0(s),null;case 25:return null;case 30:return null}throw Error(l(156,s.tag))}function kb(i,s){switch(At(s),s.tag){case 1:return i=s.flags,i&65536?(s.flags=i&-65537|128,s):null;case 3:return Y3(k0),l2(),i=s.flags,(i&65536)!==0&&(i&128)===0?(s.flags=i&-65537|128,s):null;case 26:case 27:case 5:return L2(s),null;case 31:if(s.memoizedState!==null){if(V1(s),s.alternate===null)throw Error(l(340));g5()}return i=s.flags,i&65536?(s.flags=i&-65537|128,s):null;case 13:if(V1(s),i=s.memoizedState,i!==null&&i.dehydrated!==null){if(s.alternate===null)throw Error(l(340));g5()}return i=s.flags,i&65536?(s.flags=i&-65537|128,s):null;case 19:return X(A0),null;case 4:return l2(),null;case 10:return Y3(s.type),null;case 22:case 23:return V1(s),Gt(),i!==null&&X(_5),i=s.flags,i&65536?(s.flags=i&-65537|128,s):null;case 24:return Y3(k0),null;case 25:return null;default:return null}}function Hf(i,s){switch(At(s),s.tag){case 3:Y3(k0),l2();break;case 26:case 27:case 5:L2(s);break;case 4:l2();break;case 31:s.memoizedState!==null&&V1(s);break;case 13:V1(s);break;case 19:X(A0);break;case 10:Y3(s.type);break;case 22:case 23:V1(s),Gt(),i!==null&&X(_5);break;case 24:Y3(k0)}}function B7(i,s){try{var h=s.updateQueue,v=h!==null?h.lastEffect:null;if(v!==null){var u=v.next;h=u;do{if((h.tag&i)===i){v=void 0;var m=h.create,w=h.inst;v=m(),w.destroy=v}h=h.next}while(h!==u)}}catch(A){r0(s,s.return,A)}}function k4(i,s,h){try{var v=s.updateQueue,u=v!==null?v.lastEffect:null;if(u!==null){var m=u.next;v=m;do{if((v.tag&i)===i){var w=v.inst,A=w.destroy;if(A!==void 0){w.destroy=void 0,u=s;var U=h,W=A;try{W()}catch(a2){r0(u,U,a2)}}}v=v.next}while(v!==m)}}catch(a2){r0(s,s.return,a2)}}function Rf(i){var s=i.updateQueue;if(s!==null){var h=i.stateNode;try{Ev(s,h)}catch(v){r0(i,i.return,v)}}}function Vf(i,s,h){h.props=F5(i.type,i.memoizedProps),h.state=i.memoizedState;try{h.componentWillUnmount()}catch(v){r0(i,s,v)}}function k7(i,s){try{var h=i.ref;if(h!==null){switch(i.tag){case 26:case 27:case 5:var v=i.stateNode;break;case 30:v=i.stateNode;break;default:v=i.stateNode}typeof h=="function"?i.refCleanup=h(v):h.current=v}}catch(u){r0(i,s,u)}}function y3(i,s){var h=i.ref,v=i.refCleanup;if(h!==null)if(typeof v=="function")try{v()}catch(u){r0(i,s,u)}finally{i.refCleanup=null,i=i.alternate,i!=null&&(i.refCleanup=null)}else if(typeof h=="function")try{h(null)}catch(u){r0(i,s,u)}else h.current=null}function If(i){var s=i.type,h=i.memoizedProps,v=i.stateNode;try{e:switch(s){case"button":case"input":case"select":case"textarea":h.autoFocus&&v.focus();break e;case"img":h.src?v.src=h.src:h.srcSet&&(v.srcset=h.srcSet)}}catch(u){r0(i,i.return,u)}}function Cl(i,s,h){try{var v=i.stateNode;eE(v,i.type,h,s),v[w1]=s}catch(u){r0(i,i.return,u)}}function Nf(i){return i.tag===5||i.tag===3||i.tag===26||i.tag===27&&N4(i.type)||i.tag===4}function Fl(i){e:for(;;){for(;i.sibling===null;){if(i.return===null||Nf(i.return))return null;i=i.return}for(i.sibling.return=i.return,i=i.sibling;i.tag!==5&&i.tag!==6&&i.tag!==18;){if(i.tag===27&&N4(i.type)||i.flags&2||i.child===null||i.tag===4)continue e;i.child.return=i,i=i.child}if(!(i.flags&2))return i.stateNode}}function bl(i,s,h){var v=i.tag;if(v===5||v===6)i=i.stateNode,s?(h.nodeType===9?h.body:h.nodeName==="HTML"?h.ownerDocument.body:h).insertBefore(i,s):(s=h.nodeType===9?h.body:h.nodeName==="HTML"?h.ownerDocument.body:h,s.appendChild(i),h=h._reactRootContainer,h!=null||s.onclick!==null||(s.onclick=j3));else if(v!==4&&(v===27&&N4(i.type)&&(h=i.stateNode,s=null),i=i.child,i!==null))for(bl(i,s,h),i=i.sibling;i!==null;)bl(i,s,h),i=i.sibling}function I9(i,s,h){var v=i.tag;if(v===5||v===6)i=i.stateNode,s?h.insertBefore(i,s):h.appendChild(i);else if(v!==4&&(v===27&&N4(i.type)&&(h=i.stateNode),i=i.child,i!==null))for(I9(i,s,h),i=i.sibling;i!==null;)I9(i,s,h),i=i.sibling}function Of(i){var s=i.stateNode,h=i.memoizedProps;try{for(var v=i.type,u=s.attributes;u.length;)s.removeAttributeNode(u[0]);e1(s,v,h),s[K0]=i,s[w1]=h}catch(m){r0(i,i.return,m)}}var J3=!1,H0=!1,El=!1,Uf=typeof WeakSet=="function"?WeakSet:Set,$0=null;function Lb(i,s){if(i=i.containerInfo,$l=ae,i=Qp(i),_t(i)){if("selectionStart"in i)var h={start:i.selectionStart,end:i.selectionEnd};else e:{h=(h=i.ownerDocument)&&h.defaultView||window;var v=h.getSelection&&h.getSelection();if(v&&v.rangeCount!==0){h=v.anchorNode;var u=v.anchorOffset,m=v.focusNode;v=v.focusOffset;try{h.nodeType,m.nodeType}catch{h=null;break e}var w=0,A=-1,U=-1,W=0,a2=0,d2=i,Q=null;c:for(;;){for(var t2;d2!==h||u!==0&&d2.nodeType!==3||(A=w+u),d2!==m||v!==0&&d2.nodeType!==3||(U=w+v),d2.nodeType===3&&(w+=d2.nodeValue.length),(t2=d2.firstChild)!==null;)Q=d2,d2=t2;for(;;){if(d2===i)break c;if(Q===h&&++W===u&&(A=w),Q===m&&++a2===v&&(U=w),(t2=d2.nextSibling)!==null)break;d2=Q,Q=d2.parentNode}d2=t2}h=A===-1||U===-1?null:{start:A,end:U}}else h=null}h=h||{start:0,end:0}}else h=null;for(Zl={focusedElem:i,selectionRange:h},ae=!1,$0=s;$0!==null;)if(s=$0,i=s.child,(s.subtreeFlags&1028)!==0&&i!==null)i.return=s,$0=i;else for(;$0!==null;){switch(s=$0,m=s.alternate,i=s.flags,s.tag){case 0:if((i&4)!==0&&(i=s.updateQueue,i=i!==null?i.events:null,i!==null))for(h=0;h title"))),e1(m,v,h),m[K0]=i,q0(m),v=m;break e;case"link":var w=Zg("link","href",u).get(v+(h.href||""));if(w){for(var A=0;Ad0&&(w=d0,d0=D2,D2=w);var Z=Kp(A,D2),q=Kp(A,d0);if(Z&&q&&(t2.rangeCount!==1||t2.anchorNode!==Z.node||t2.anchorOffset!==Z.offset||t2.focusNode!==q.node||t2.focusOffset!==q.offset)){var Y=d2.createRange();Y.setStart(Z.node,Z.offset),t2.removeAllRanges(),D2>d0?(t2.addRange(Y),t2.extend(q.node,q.offset)):(Y.setEnd(q.node,q.offset),t2.addRange(Y))}}}}for(d2=[],t2=A;t2=t2.parentNode;)t2.nodeType===1&&d2.push({element:t2,left:t2.scrollLeft,top:t2.scrollTop});for(typeof A.focus=="function"&&A.focus(),A=0;Ah?32:h,$.T=null,h=kl,kl=null;var m=R4,w=i4;if(U0=0,u6=R4=null,i4=0,(l0&6)!==0)throw Error(l(331));var A=l0;if(l0|=4,Qf(m.current),Wf(m,m.current,w,h),l0=A,I7(0,!1),O0&&typeof O0.onPostCommitFiberRoot=="function")try{O0.onPostCommitFiberRoot(d1,m)}catch{}return!0}finally{e2.p=u,$.T=v,ug(i,s)}}function _g(i,s,h){s=W1(h,s),s=dl(i.stateNode,s,2),i=A4(i,s,2),i!==null&&(i7(i,2),D3(i))}function r0(i,s,h){if(i.tag===3)_g(i,i,h);else for(;s!==null;){if(s.tag===3){_g(s,i,h);break}else if(s.tag===1){var v=s.stateNode;if(typeof s.type.getDerivedStateFromError=="function"||typeof v.componentDidCatch=="function"&&(H4===null||!H4.has(v))){i=W1(h,i),h=zf(2),v=A4(s,h,2),v!==null&&(Mf(h,v,s,i),i7(v,2),D3(v));break}}s=s.return}}function Rl(i,s,h){var v=i.pingCache;if(v===null){v=i.pingCache=new Rb;var u=new Set;v.set(s,u)}else u=v.get(s),u===void 0&&(u=new Set,v.set(s,u));u.has(h)||(Dl=!0,u.add(h),i=Ub.bind(null,i,s,h),s.then(i,i))}function Ub(i,s,h){var v=i.pingCache;v!==null&&v.delete(s),i.pingedLanes|=i.suspendedLanes&h,i.warmLanes&=~h,f0===i&&(Y2&h)===h&&(E0===4||E0===3&&(Y2&62914560)===Y2&&300>h1()-U9?(l0&2)===0&&m6(i,0):Al|=h,g6===Y2&&(g6=0)),D3(i)}function zg(i,s){s===0&&(s=pp()),i=v5(i,s),i!==null&&(i7(i,s),D3(i))}function Pb(i){var s=i.memoizedState,h=0;s!==null&&(h=s.retryLane),zg(i,h)}function jb(i,s){var h=0;switch(i.tag){case 31:case 13:var v=i.stateNode,u=i.memoizedState;u!==null&&(h=u.retryLane);break;case 19:v=i.stateNode;break;case 22:v=i.stateNode._retryCache;break;default:throw Error(l(314))}v!==null&&v.delete(s),zg(i,h)}function Gb(i,s){return h3(i,s)}var Y9=null,z6=null,Vl=!1,W9=!1,Il=!1,I4=0;function D3(i){i!==z6&&i.next===null&&(z6===null?Y9=z6=i:z6=z6.next=i),W9=!0,Vl||(Vl=!0,$b())}function I7(i,s){if(!Il&&W9){Il=!0;do for(var h=!1,v=Y9;v!==null;){if(i!==0){var u=v.pendingLanes;if(u===0)var m=0;else{var w=v.suspendedLanes,A=v.pingedLanes;m=(1<<31-v0(42|i)+1)-1,m&=u&~(w&~A),m=m&201326741?m&201326741|1:m?m|2:0}m!==0&&(h=!0,Fg(v,m))}else m=Y2,m=J8(v,v===f0?m:0,v.cancelPendingCommit!==null||v.timeoutHandle!==-1),(m&3)===0||l7(v,m)||(h=!0,Fg(v,m));v=v.next}while(h);Il=!1}}function qb(){Mg()}function Mg(){W9=Vl=!1;var i=0;I4!==0&&tE()&&(i=I4);for(var s=h1(),h=null,v=Y9;v!==null;){var u=v.next,m=wg(v,s);m===0?(v.next=null,h===null?Y9=u:h.next=u,u===null&&(z6=h)):(h=v,(i!==0||(m&3)!==0)&&(W9=!0)),v=u}U0!==0&&U0!==5||I7(i),I4!==0&&(I4=0)}function wg(i,s){for(var h=i.suspendedLanes,v=i.pingedLanes,u=i.expirationTimes,m=i.pendingLanes&-62914561;0A)break;var a2=U.transferSize,d2=U.initiatorType;a2&&Bg(d2)&&(U=U.responseEnd,w+=a2*(U"u"?null:document;function jg(i,s,h){var v=M6;if(v&&typeof s=="string"&&s){var u=Z1(s);u='link[rel="'+i+'"][href="'+u+'"]',typeof h=="string"&&(u+='[crossorigin="'+h+'"]'),Pg.has(u)||(Pg.add(u),i={rel:i,crossOrigin:h,href:s},v.querySelector(u)===null&&(s=v.createElement("link"),e1(s,"link",i),q0(s),v.head.appendChild(s)))}}function dE(i){a4.D(i),jg("dns-prefetch",i,null)}function pE(i,s){a4.C(i,s),jg("preconnect",i,s)}function vE(i,s,h){a4.L(i,s,h);var v=M6;if(v&&i&&s){var u='link[rel="preload"][as="'+Z1(s)+'"]';s==="image"&&h&&h.imageSrcSet?(u+='[imagesrcset="'+Z1(h.imageSrcSet)+'"]',typeof h.imageSizes=="string"&&(u+='[imagesizes="'+Z1(h.imageSizes)+'"]')):u+='[href="'+Z1(i)+'"]';var m=u;switch(s){case"style":m=w6(i);break;case"script":m=C6(i)}c3.has(m)||(i=g({rel:"preload",href:s==="image"&&h&&h.imageSrcSet?void 0:i,as:s},h),c3.set(m,i),v.querySelector(u)!==null||s==="style"&&v.querySelector(P7(m))||s==="script"&&v.querySelector(j7(m))||(s=v.createElement("link"),e1(s,"link",i),q0(s),v.head.appendChild(s)))}}function fE(i,s){a4.m(i,s);var h=M6;if(h&&i){var v=s&&typeof s.as=="string"?s.as:"script",u='link[rel="modulepreload"][as="'+Z1(v)+'"][href="'+Z1(i)+'"]',m=u;switch(v){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":m=C6(i)}if(!c3.has(m)&&(i=g({rel:"modulepreload",href:i},s),c3.set(m,i),h.querySelector(u)===null)){switch(v){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(h.querySelector(j7(m)))return}v=h.createElement("link"),e1(v,"link",i),q0(v),h.head.appendChild(v)}}}function gE(i,s,h){a4.S(i,s,h);var v=M6;if(v&&i){var u=j5(v).hoistableStyles,m=w6(i);s=s||"default";var w=u.get(m);if(!w){var A={loading:0,preload:null};if(w=v.querySelector(P7(m)))A.loading=5;else{i=g({rel:"stylesheet",href:i,"data-precedence":s},h),(h=c3.get(m))&&ei(i,h);var U=w=v.createElement("link");q0(U),e1(U,"link",i),U._p=new Promise(function(W,a2){U.onload=W,U.onerror=a2}),U.addEventListener("load",function(){A.loading|=1}),U.addEventListener("error",function(){A.loading|=2}),A.loading|=4,ee(w,s,v)}w={type:"stylesheet",instance:w,count:1,state:A},u.set(m,w)}}}function uE(i,s){a4.X(i,s);var h=M6;if(h&&i){var v=j5(h).hoistableScripts,u=C6(i),m=v.get(u);m||(m=h.querySelector(j7(u)),m||(i=g({src:i,async:!0},s),(s=c3.get(u))&&ci(i,s),m=h.createElement("script"),q0(m),e1(m,"link",i),h.head.appendChild(m)),m={type:"script",instance:m,count:1,state:null},v.set(u,m))}}function mE(i,s){a4.M(i,s);var h=M6;if(h&&i){var v=j5(h).hoistableScripts,u=C6(i),m=v.get(u);m||(m=h.querySelector(j7(u)),m||(i=g({src:i,async:!0,type:"module"},s),(s=c3.get(u))&&ci(i,s),m=h.createElement("script"),q0(m),e1(m,"link",i),h.head.appendChild(m)),m={type:"script",instance:m,count:1,state:null},v.set(u,m))}}function Gg(i,s,h,v){var u=(u=M2.current)?J9(u):null;if(!u)throw Error(l(446));switch(i){case"meta":case"title":return null;case"style":return typeof h.precedence=="string"&&typeof h.href=="string"?(s=w6(h.href),h=j5(u).hoistableStyles,v=h.get(s),v||(v={type:"style",instance:null,count:0,state:null},h.set(s,v)),v):{type:"void",instance:null,count:0,state:null};case"link":if(h.rel==="stylesheet"&&typeof h.href=="string"&&typeof h.precedence=="string"){i=w6(h.href);var m=j5(u).hoistableStyles,w=m.get(i);if(w||(u=u.ownerDocument||u,w={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},m.set(i,w),(m=u.querySelector(P7(i)))&&!m._p&&(w.instance=m,w.state.loading=5),c3.has(i)||(h={rel:"preload",as:"style",href:h.href,crossOrigin:h.crossOrigin,integrity:h.integrity,media:h.media,hrefLang:h.hrefLang,referrerPolicy:h.referrerPolicy},c3.set(i,h),m||_E(u,i,h,w.state))),s&&v===null)throw Error(l(528,""));return w}if(s&&v!==null)throw Error(l(529,""));return null;case"script":return s=h.async,h=h.src,typeof h=="string"&&s&&typeof s!="function"&&typeof s!="symbol"?(s=C6(h),h=j5(u).hoistableScripts,v=h.get(s),v||(v={type:"script",instance:null,count:0,state:null},h.set(s,v)),v):{type:"void",instance:null,count:0,state:null};default:throw Error(l(444,i))}}function w6(i){return'href="'+Z1(i)+'"'}function P7(i){return'link[rel="stylesheet"]['+i+"]"}function qg(i){return g({},i,{"data-precedence":i.precedence,precedence:null})}function _E(i,s,h,v){i.querySelector('link[rel="preload"][as="style"]['+s+"]")?v.loading=1:(s=i.createElement("link"),v.preload=s,s.addEventListener("load",function(){return v.loading|=1}),s.addEventListener("error",function(){return v.loading|=2}),e1(s,"link",h),q0(s),i.head.appendChild(s))}function C6(i){return'[src="'+Z1(i)+'"]'}function j7(i){return"script[async]"+i}function $g(i,s,h){if(s.count++,s.instance===null)switch(s.type){case"style":var v=i.querySelector('style[data-href~="'+Z1(h.href)+'"]');if(v)return s.instance=v,q0(v),v;var u=g({},h,{"data-href":h.href,"data-precedence":h.precedence,href:null,precedence:null});return v=(i.ownerDocument||i).createElement("style"),q0(v),e1(v,"style",u),ee(v,h.precedence,i),s.instance=v;case"stylesheet":u=w6(h.href);var m=i.querySelector(P7(u));if(m)return s.state.loading|=4,s.instance=m,q0(m),m;v=qg(h),(u=c3.get(u))&&ei(v,u),m=(i.ownerDocument||i).createElement("link"),q0(m);var w=m;return w._p=new Promise(function(A,U){w.onload=A,w.onerror=U}),e1(m,"link",v),s.state.loading|=4,ee(m,h.precedence,i),s.instance=m;case"script":return m=C6(h.src),(u=i.querySelector(j7(m)))?(s.instance=u,q0(u),u):(v=h,(u=c3.get(m))&&(v=g({},h),ci(v,u)),i=i.ownerDocument||i,u=i.createElement("script"),q0(u),e1(u,"link",v),i.head.appendChild(u),s.instance=u);case"void":return null;default:throw Error(l(443,s.type))}else s.type==="stylesheet"&&(s.state.loading&4)===0&&(v=s.instance,s.state.loading|=4,ee(v,h.precedence,i));return s.instance}function ee(i,s,h){for(var v=h.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),u=v.length?v[v.length-1]:null,m=u,w=0;w title"):null)}function zE(i,s,h){if(h===1||s.itemProp!=null)return!1;switch(i){case"meta":case"title":return!0;case"style":if(typeof s.precedence!="string"||typeof s.href!="string"||s.href==="")break;return!0;case"link":if(typeof s.rel!="string"||typeof s.href!="string"||s.href===""||s.onLoad||s.onError)break;return s.rel==="stylesheet"?(i=s.disabled,typeof s.precedence=="string"&&i==null):!0;case"script":if(s.async&&typeof s.async!="function"&&typeof s.async!="symbol"&&!s.onLoad&&!s.onError&&s.src&&typeof s.src=="string")return!0}return!1}function Wg(i){return!(i.type==="stylesheet"&&(i.state.loading&3)===0)}function ME(i,s,h,v){if(h.type==="stylesheet"&&(typeof v.media!="string"||matchMedia(v.media).matches!==!1)&&(h.state.loading&4)===0){if(h.instance===null){var u=w6(v.href),m=s.querySelector(P7(u));if(m){s=m._p,s!==null&&typeof s=="object"&&typeof s.then=="function"&&(i.count++,i=te.bind(i),s.then(i,i)),h.state.loading|=4,h.instance=m,q0(m);return}m=s.ownerDocument||s,v=qg(v),(u=c3.get(u))&&ei(v,u),m=m.createElement("link"),q0(m);var w=m;w._p=new Promise(function(A,U){w.onload=A,w.onerror=U}),e1(m,"link",v),h.instance=m}i.stylesheets===null&&(i.stylesheets=new Map),i.stylesheets.set(h,s),(s=h.state.preload)&&(h.state.loading&3)===0&&(i.count++,h=te.bind(i),s.addEventListener("load",h),s.addEventListener("error",h))}}var ti=0;function wE(i,s){return i.stylesheets&&i.count===0&&ie(i,i.stylesheets),0ti?50:800)+s);return i.unsuspend=h,function(){i.unsuspend=null,clearTimeout(v),clearTimeout(u)}}:null}function te(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ie(this,this.stylesheets);else if(this.unsuspend){var i=this.unsuspend;this.unsuspend=null,i()}}}var le=null;function ie(i,s){i.stylesheets=null,i.unsuspend!==null&&(i.count++,le=new Map,s.forEach(CE,i),le=null,te.call(i))}function CE(i,s){if(!(s.state.loading&4)){var h=le.get(i);if(h)var v=h.get(null);else{h=new Map,le.set(i,h);for(var u=i.querySelectorAll("link[data-precedence],style[data-precedence]"),m=0;m"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(c){console.error(c)}}return e(),di.exports=IE(),di.exports}var OE=NE();const UE=gc(OE);var zu="popstate";function PE(e={}){function c(a,n){let{pathname:r="/",search:o="",hash:d=""}=R5(a.location.hash.substring(1));return!r.startsWith("/")&&!r.startsWith(".")&&(r="/"+r),sa("",{pathname:r,search:o,hash:d},n.state&&n.state.usr||null,n.state&&n.state.key||"default")}function t(a,n){let r=a.document.querySelector("base"),o="";if(r&&r.getAttribute("href")){let d=a.location.href,p=d.indexOf("#");o=p===-1?d:d.slice(0,p)}return o+"#"+(typeof n=="string"?n:C8(n))}function l(a,n){r3(a.pathname.charAt(0)==="/",`relative pathnames are not supported in hash history.push(${JSON.stringify(n)})`)}return GE(c,t,l,e)}function F0(e,c){if(e===!1||e===null||typeof e>"u")throw new Error(c)}function r3(e,c){if(!e){typeof console<"u"&&console.warn(c);try{throw new Error(c)}catch{}}}function jE(){return Math.random().toString(36).substring(2,10)}function Mu(e,c){return{usr:e.state,key:e.key,idx:c}}function sa(e,c,t=null,l){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof c=="string"?R5(c):c,state:t,key:c&&c.key||l||jE()}}function C8({pathname:e="/",search:c="",hash:t=""}){return c&&c!=="?"&&(e+=c.charAt(0)==="?"?c:"?"+c),t&&t!=="#"&&(e+=t.charAt(0)==="#"?t:"#"+t),e}function R5(e){let c={};if(e){let t=e.indexOf("#");t>=0&&(c.hash=e.substring(t),e=e.substring(0,t));let l=e.indexOf("?");l>=0&&(c.search=e.substring(l),e=e.substring(0,l)),e&&(c.pathname=e)}return c}function GE(e,c,t,l={}){let{window:a=document.defaultView,v5Compat:n=!1}=l,r=a.history,o="POP",d=null,p=f();p==null&&(p=0,r.replaceState({...r.state,idx:p},""));function f(){return(r.state||{idx:null}).idx}function g(){o="POP";let x=f(),F=x==null?null:x-p;p=x,d&&d({action:o,location:C.location,delta:F})}function _(x,F){o="PUSH";let L=sa(C.location,x,F);t&&t(L,x),p=f()+1;let S=Mu(L,p),b=C.createHref(L);try{r.pushState(S,"",b)}catch(E){if(E instanceof DOMException&&E.name==="DataCloneError")throw E;a.location.assign(b)}n&&d&&d({action:o,location:C.location,delta:1})}function z(x,F){o="REPLACE";let L=sa(C.location,x,F);t&&t(L,x),p=f();let S=Mu(L,p),b=C.createHref(L);r.replaceState(S,"",b),n&&d&&d({action:o,location:C.location,delta:0})}function M(x){return qE(x)}let C={get action(){return o},get location(){return e(a,r)},listen(x){if(d)throw new Error("A history only accepts one active listener");return a.addEventListener(zu,g),d=x,()=>{a.removeEventListener(zu,g),d=null}},createHref(x){return c(a,x)},createURL:M,encodeLocation(x){let F=M(x);return{pathname:F.pathname,search:F.search,hash:F.hash}},push:_,replace:z,go(x){return r.go(x)}};return C}function qE(e,c=!1){let t="http://localhost";typeof window<"u"&&(t=window.location.origin!=="null"?window.location.origin:window.location.href),F0(t,"No window.location.(origin|href) available to create URL");let l=typeof e=="string"?e:C8(e);return l=l.replace(/ $/,"%20"),!c&&l.startsWith("//")&&(l=t+l),new URL(l,t)}function q_(e,c,t="/"){return $E(e,c,t,!1)}function $E(e,c,t,l){let a=typeof c=="string"?R5(c):c,n=f4(a.pathname||"/",t);if(n==null)return null;let r=$_(e);ZE(r);let o=null;for(let d=0;o==null&&d{let f={relativePath:p===void 0?r.path||"":p,caseSensitive:r.caseSensitive===!0,childrenIndex:o,route:r};if(f.relativePath.startsWith("/")){if(!f.relativePath.startsWith(l)&&d)return;F0(f.relativePath.startsWith(l),`Absolute route path "${f.relativePath}" nested under path "${l}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),f.relativePath=f.relativePath.slice(l.length)}let g=v4([l,f.relativePath]),_=t.concat(f);r.children&&r.children.length>0&&(F0(r.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${g}".`),$_(r.children,c,_,g,d)),!(r.path==null&&!r.index)&&c.push({path:g,score:ex(g,r.index),routesMeta:_})};return e.forEach((r,o)=>{if(r.path===""||!r.path?.includes("?"))n(r,o);else for(let d of Z_(r.path))n(r,o,!0,d)}),c}function Z_(e){let c=e.split("/");if(c.length===0)return[];let[t,...l]=c,a=t.endsWith("?"),n=t.replace(/\?$/,"");if(l.length===0)return a?[n,""]:[n];let r=Z_(l.join("/")),o=[];return o.push(...r.map(d=>d===""?n:[n,d].join("/"))),a&&o.push(...r),o.map(d=>e.startsWith("/")&&d===""?"/":d)}function ZE(e){e.sort((c,t)=>c.score!==t.score?t.score-c.score:cx(c.routesMeta.map(l=>l.childrenIndex),t.routesMeta.map(l=>l.childrenIndex)))}var YE=/^:[\w-]+$/,WE=3,KE=2,XE=1,QE=10,JE=-2,wu=e=>e==="*";function ex(e,c){let t=e.split("/"),l=t.length;return t.some(wu)&&(l+=JE),c&&(l+=KE),t.filter(a=>!wu(a)).reduce((a,n)=>a+(YE.test(n)?WE:n===""?XE:QE),l)}function cx(e,c){return e.length===c.length&&e.slice(0,-1).every((l,a)=>l===c[a])?e[e.length-1]-c[c.length-1]:0}function tx(e,c,t=!1){let{routesMeta:l}=e,a={},n="/",r=[];for(let o=0;o{if(f==="*"){let M=o[_]||"";r=n.slice(0,n.length-M.length).replace(/(.)\/+$/,"$1")}const z=o[_];return g&&!z?p[f]=void 0:p[f]=(z||"").replace(/%2F/g,"/"),p},{}),pathname:n,pathnameBase:r,pattern:e}}function lx(e,c=!1,t=!0){r3(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let l=[],a="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(r,o,d)=>(l.push({paramName:o,isOptional:d!=null}),d?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(l.push({paramName:"*"}),a+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):t?a+="\\/*$":e!==""&&e!=="/"&&(a+="(?:(?=\\/|$))"),[new RegExp(a,c?void 0:"i"),l]}function ix(e){try{return e.split("/").map(c=>decodeURIComponent(c).replace(/\//g,"%2F")).join("/")}catch(c){return r3(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${c}).`),e}}function f4(e,c){if(c==="/")return e;if(!e.toLowerCase().startsWith(c.toLowerCase()))return null;let t=c.endsWith("/")?c.length-1:c.length,l=e.charAt(t);return l&&l!=="/"?null:e.slice(t)||"/"}var Y_=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,ax=e=>Y_.test(e);function sx(e,c="/"){let{pathname:t,search:l="",hash:a=""}=typeof e=="string"?R5(e):e,n;if(t)if(ax(t))n=t;else{if(t.includes("//")){let r=t;t=t.replace(/\/\/+/g,"/"),r3(!1,`Pathnames cannot have embedded double slashes - normalizing ${r} -> ${t}`)}t.startsWith("/")?n=Cu(t.substring(1),"/"):n=Cu(t,c)}else n=c;return{pathname:n,search:ox(l),hash:hx(a)}}function Cu(e,c){let t=c.replace(/\/+$/,"").split("/");return e.split("/").forEach(a=>{a===".."?t.length>1&&t.pop():a!=="."&&t.push(a)}),t.length>1?t.join("/"):"/"}function gi(e,c,t,l){return`Cannot include a '${e}' character in a manually specified \`to.${c}\` field [${JSON.stringify(l)}]. Please separate it out to the \`to.${t}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function nx(e){return e.filter((c,t)=>t===0||c.route.path&&c.route.path.length>0)}function W_(e){let c=nx(e);return c.map((t,l)=>l===c.length-1?t.pathname:t.pathnameBase)}function K_(e,c,t,l=!1){let a;typeof e=="string"?a=R5(e):(a={...e},F0(!a.pathname||!a.pathname.includes("?"),gi("?","pathname","search",a)),F0(!a.pathname||!a.pathname.includes("#"),gi("#","pathname","hash",a)),F0(!a.search||!a.search.includes("#"),gi("#","search","hash",a)));let n=e===""||a.pathname==="",r=n?"/":a.pathname,o;if(r==null)o=t;else{let g=c.length-1;if(!l&&r.startsWith("..")){let _=r.split("/");for(;_[0]==="..";)_.shift(),g-=1;a.pathname=_.join("/")}o=g>=0?c[g]:"/"}let d=sx(a,o),p=r&&r!=="/"&&r.endsWith("/"),f=(n||r===".")&&t.endsWith("/");return!d.pathname.endsWith("/")&&(p||f)&&(d.pathname+="/"),d}var v4=e=>e.join("/").replace(/\/\/+/g,"/"),rx=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),ox=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,hx=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,dx=class{constructor(e,c,t,l=!1){this.status=e,this.statusText=c||"",this.internal=l,t instanceof Error?(this.data=t.toString(),this.error=t):this.data=t}};function px(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function vx(e){return e.map(c=>c.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var X_=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Q_(e,c){let t=e;if(typeof t!="string"||!Y_.test(t))return{absoluteURL:void 0,isExternal:!1,to:t};let l=t,a=!1;if(X_)try{let n=new URL(window.location.href),r=t.startsWith("//")?new URL(n.protocol+t):new URL(t),o=f4(r.pathname,c);r.origin===n.origin&&o!=null?t=o+r.search+r.hash:a=!0}catch{r3(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:l,isExternal:a,to:t}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var J_=["POST","PUT","PATCH","DELETE"];new Set(J_);var fx=["GET",...J_];new Set(fx);var Z6=V.createContext(null);Z6.displayName="DataRouter";var uc=V.createContext(null);uc.displayName="DataRouterState";var gx=V.createContext(!1),ez=V.createContext({isTransitioning:!1});ez.displayName="ViewTransition";var ux=V.createContext(new Map);ux.displayName="Fetchers";var mx=V.createContext(null);mx.displayName="Await";var o3=V.createContext(null);o3.displayName="Navigation";var H8=V.createContext(null);H8.displayName="Location";var u4=V.createContext({outlet:null,matches:[],isDataRoute:!1});u4.displayName="Route";var ss=V.createContext(null);ss.displayName="RouteError";var cz="REACT_ROUTER_ERROR",_x="REDIRECT",zx="ROUTE_ERROR_RESPONSE";function Mx(e){if(e.startsWith(`${cz}:${_x}:{`))try{let c=JSON.parse(e.slice(28));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string"&&typeof c.location=="string"&&typeof c.reloadDocument=="boolean"&&typeof c.replace=="boolean")return c}catch{}}function wx(e){if(e.startsWith(`${cz}:${zx}:{`))try{let c=JSON.parse(e.slice(40));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string")return new dx(c.status,c.statusText,c.data)}catch{}}function Cx(e,{relative:c}={}){F0(R8(),"useHref() may be used only in the context of a component.");let{basename:t,navigator:l}=V.useContext(o3),{hash:a,pathname:n,search:r}=V8(e,{relative:c}),o=n;return t!=="/"&&(o=n==="/"?t:v4([t,n])),l.createHref({pathname:o,search:r,hash:a})}function R8(){return V.useContext(H8)!=null}function i5(){return F0(R8(),"useLocation() may be used only in the context of a component."),V.useContext(H8).location}var tz="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function lz(e){V.useContext(o3).static||V.useLayoutEffect(e)}function ns(){let{isDataRoute:e}=V.useContext(u4);return e?Hx():Fx()}function Fx(){F0(R8(),"useNavigate() may be used only in the context of a component.");let e=V.useContext(Z6),{basename:c,navigator:t}=V.useContext(o3),{matches:l}=V.useContext(u4),{pathname:a}=i5(),n=JSON.stringify(W_(l)),r=V.useRef(!1);return lz(()=>{r.current=!0}),V.useCallback((d,p={})=>{if(r3(r.current,tz),!r.current)return;if(typeof d=="number"){t.go(d);return}let f=K_(d,JSON.parse(n),a,p.relative==="path");e==null&&c!=="/"&&(f.pathname=f.pathname==="/"?c:v4([c,f.pathname])),(p.replace?t.replace:t.push)(f,p.state,p)},[c,t,n,a,e])}V.createContext(null);function V8(e,{relative:c}={}){let{matches:t}=V.useContext(u4),{pathname:l}=i5(),a=JSON.stringify(W_(t));return V.useMemo(()=>K_(e,JSON.parse(a),l,c==="path"),[e,a,l,c])}function bx(e,c){return iz(e,c)}function iz(e,c,t,l,a){F0(R8(),"useRoutes() may be used only in the context of a component.");let{navigator:n}=V.useContext(o3),{matches:r}=V.useContext(u4),o=r[r.length-1],d=o?o.params:{},p=o?o.pathname:"/",f=o?o.pathnameBase:"/",g=o&&o.route;{let L=g&&g.path||"";sz(p,!g||L.endsWith("*")||L.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${p}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let _=i5(),z;if(c){let L=typeof c=="string"?R5(c):c;F0(f==="/"||L.pathname?.startsWith(f),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${f}" but pathname "${L.pathname}" was given in the \`location\` prop.`),z=L}else z=_;let M=z.pathname||"/",C=M;if(f!=="/"){let L=f.replace(/^\//,"").split("/");C="/"+M.replace(/^\//,"").split("/").slice(L.length).join("/")}let x=q_(e,{pathname:C});r3(g||x!=null,`No routes matched location "${z.pathname}${z.search}${z.hash}" `),r3(x==null||x[x.length-1].route.element!==void 0||x[x.length-1].route.Component!==void 0||x[x.length-1].route.lazy!==void 0,`Matched leaf route at location "${z.pathname}${z.search}${z.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let F=Ax(x&&x.map(L=>Object.assign({},L,{params:Object.assign({},d,L.params),pathname:v4([f,n.encodeLocation?n.encodeLocation(L.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathname]),pathnameBase:L.pathnameBase==="/"?f:v4([f,n.encodeLocation?n.encodeLocation(L.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathnameBase])})),r,t,l,a);return c&&F?V.createElement(H8.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...z},navigationType:"POP"}},F):F}function Ex(){let e=Tx(),c=px(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),t=e instanceof Error?e.stack:null,l="rgba(200,200,200, 0.5)",a={padding:"0.5rem",backgroundColor:l},n={padding:"2px 4px",backgroundColor:l},r=null;return console.error("Error handled by React Router default ErrorBoundary:",e),r=V.createElement(V.Fragment,null,V.createElement("p",null,"💿 Hey developer 👋"),V.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",V.createElement("code",{style:n},"ErrorBoundary")," or"," ",V.createElement("code",{style:n},"errorElement")," prop on your route.")),V.createElement(V.Fragment,null,V.createElement("h2",null,"Unexpected Application Error!"),V.createElement("h3",{style:{fontStyle:"italic"}},c),t?V.createElement("pre",{style:a},t):null,r)}var xx=V.createElement(Ex,null),az=class extends V.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,c){return c.location!==e.location||c.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:c.error,location:c.location,revalidation:e.revalidation||c.revalidation}}componentDidCatch(e,c){this.props.onError?this.props.onError(e,c):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const t=wx(e.digest);t&&(e=t)}let c=e!==void 0?V.createElement(u4.Provider,{value:this.props.routeContext},V.createElement(ss.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?V.createElement(yx,{error:e},c):c}};az.contextType=gx;var ui=new WeakMap;function yx({children:e,error:c}){let{basename:t}=V.useContext(o3);if(typeof c=="object"&&c&&"digest"in c&&typeof c.digest=="string"){let l=Mx(c.digest);if(l){let a=ui.get(c);if(a)throw a;let n=Q_(l.location,t);if(X_&&!ui.get(c))if(n.isExternal||l.reloadDocument)window.location.href=n.absoluteURL||n.to;else{const r=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(n.to,{replace:l.replace}));throw ui.set(c,r),r}return V.createElement("meta",{httpEquiv:"refresh",content:`0;url=${n.absoluteURL||n.to}`})}}return e}function Dx({routeContext:e,match:c,children:t}){let l=V.useContext(Z6);return l&&l.static&&l.staticContext&&(c.route.errorElement||c.route.ErrorBoundary)&&(l.staticContext._deepestRenderedBoundaryId=c.route.id),V.createElement(u4.Provider,{value:e},t)}function Ax(e,c=[],t=null,l=null,a=null){if(e==null){if(!t)return null;if(t.errors)e=t.matches;else if(c.length===0&&!t.initialized&&t.matches.length>0)e=t.matches;else return null}let n=e,r=t?.errors;if(r!=null){let f=n.findIndex(g=>g.route.id&&r?.[g.route.id]!==void 0);F0(f>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(r).join(",")}`),n=n.slice(0,Math.min(n.length,f+1))}let o=!1,d=-1;if(t)for(let f=0;f=0?n=n.slice(0,d+1):n=[n[0]];break}}}let p=t&&l?(f,g)=>{l(f,{location:t.location,params:t.matches?.[0]?.params??{},unstable_pattern:vx(t.matches),errorInfo:g})}:void 0;return n.reduceRight((f,g,_)=>{let z,M=!1,C=null,x=null;t&&(z=r&&g.route.id?r[g.route.id]:void 0,C=g.route.errorElement||xx,o&&(d<0&&_===0?(sz("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),M=!0,x=null):d===_&&(M=!0,x=g.route.hydrateFallbackElement||null)));let F=c.concat(n.slice(0,_+1)),L=()=>{let S;return z?S=C:M?S=x:g.route.Component?S=V.createElement(g.route.Component,null):g.route.element?S=g.route.element:S=f,V.createElement(Dx,{match:g,routeContext:{outlet:f,matches:F,isDataRoute:t!=null},children:S})};return t&&(g.route.ErrorBoundary||g.route.errorElement||_===0)?V.createElement(az,{location:t.location,revalidation:t.revalidation,component:C,error:z,children:L(),routeContext:{outlet:null,matches:F,isDataRoute:!0},onError:p}):L()},null)}function rs(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Sx(e){let c=V.useContext(Z6);return F0(c,rs(e)),c}function Bx(e){let c=V.useContext(uc);return F0(c,rs(e)),c}function kx(e){let c=V.useContext(u4);return F0(c,rs(e)),c}function os(e){let c=kx(e),t=c.matches[c.matches.length-1];return F0(t.route.id,`${e} can only be used on routes that contain a unique "id"`),t.route.id}function Lx(){return os("useRouteId")}function Tx(){let e=V.useContext(ss),c=Bx("useRouteError"),t=os("useRouteError");return e!==void 0?e:c.errors?.[t]}function Hx(){let{router:e}=Sx("useNavigate"),c=os("useNavigate"),t=V.useRef(!1);return lz(()=>{t.current=!0}),V.useCallback(async(a,n={})=>{r3(t.current,tz),t.current&&(typeof a=="number"?await e.navigate(a):await e.navigate(a,{fromRouteId:c,...n}))},[e,c])}var Fu={};function sz(e,c,t){!c&&!Fu[e]&&(Fu[e]=!0,r3(!1,t))}V.memo(Rx);function Rx({routes:e,future:c,state:t,onError:l}){return iz(e,void 0,t,l,c)}function xe(e){F0(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Vx({basename:e="/",children:c=null,location:t,navigationType:l="POP",navigator:a,static:n=!1,unstable_useTransitions:r}){F0(!R8(),"You cannot render a inside another . You should never have more than one in your app.");let o=e.replace(/^\/*/,"/"),d=V.useMemo(()=>({basename:o,navigator:a,static:n,unstable_useTransitions:r,future:{}}),[o,a,n,r]);typeof t=="string"&&(t=R5(t));let{pathname:p="/",search:f="",hash:g="",state:_=null,key:z="default"}=t,M=V.useMemo(()=>{let C=f4(p,o);return C==null?null:{location:{pathname:C,search:f,hash:g,state:_,key:z},navigationType:l}},[o,p,f,g,_,z,l]);return r3(M!=null,` is not able to match the URL "${p}${f}${g}" because it does not start with the basename, so the won't render anything.`),M==null?null:V.createElement(o3.Provider,{value:d},V.createElement(H8.Provider,{children:c,value:M}))}function Ix({children:e,location:c}){return bx(na(e),c)}function na(e,c=[]){let t=[];return V.Children.forEach(e,(l,a)=>{if(!V.isValidElement(l))return;let n=[...c,a];if(l.type===V.Fragment){t.push.apply(t,na(l.props.children,n));return}F0(l.type===xe,`[${typeof l.type=="string"?l.type:l.type.name}] is not a component. All component children of must be a or `),F0(!l.props.index||!l.props.children,"An index route cannot have child routes.");let r={id:l.props.id||n.join("-"),caseSensitive:l.props.caseSensitive,element:l.props.element,Component:l.props.Component,index:l.props.index,path:l.props.path,middleware:l.props.middleware,loader:l.props.loader,action:l.props.action,hydrateFallbackElement:l.props.hydrateFallbackElement,HydrateFallback:l.props.HydrateFallback,errorElement:l.props.errorElement,ErrorBoundary:l.props.ErrorBoundary,hasErrorBoundary:l.props.hasErrorBoundary===!0||l.props.ErrorBoundary!=null||l.props.errorElement!=null,shouldRevalidate:l.props.shouldRevalidate,handle:l.props.handle,lazy:l.props.lazy};l.props.children&&(r.children=na(l.props.children,n)),t.push(r)}),t}var ye="get",De="application/x-www-form-urlencoded";function mc(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function Nx(e){return mc(e)&&e.tagName.toLowerCase()==="button"}function Ox(e){return mc(e)&&e.tagName.toLowerCase()==="form"}function Ux(e){return mc(e)&&e.tagName.toLowerCase()==="input"}function Px(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function jx(e,c){return e.button===0&&(!c||c==="_self")&&!Px(e)}var pe=null;function Gx(){if(pe===null)try{new FormData(document.createElement("form"),0),pe=!1}catch{pe=!0}return pe}var qx=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function mi(e){return e!=null&&!qx.has(e)?(r3(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${De}"`),null):e}function $x(e,c){let t,l,a,n,r;if(Ox(e)){let o=e.getAttribute("action");l=o?f4(o,c):null,t=e.getAttribute("method")||ye,a=mi(e.getAttribute("enctype"))||De,n=new FormData(e)}else if(Nx(e)||Ux(e)&&(e.type==="submit"||e.type==="image")){let o=e.form;if(o==null)throw new Error('Cannot submit a + + + {activeTab === 'Overview' && ( + <> +
+ + setName(e.target.value)} + style={{ + width: '100%', + backgroundColor: '#202225', + border: 'none', + borderRadius: '4px', + padding: '10px', + color: '#dcddde', + fontSize: '16px', + outline: 'none' + }} + /> +
+ +
+ +
+ + )} + + {activeTab === 'Delete' && ( +
+

Are you sure?

+

+ Deleting #{channel.name} cannot be undone. All messages and keys will be lost forever. +

+ +
+ )} + + + +
+ {/* Right side spacer like real Discord */} +
+ + ); +}; + +export default ChannelSettingsModal; diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index 9040612..fe88c62 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -4,6 +4,26 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { + GifIcon, + EmojieIcon, + StickerIcon, + EmojiesColored, + EmojiesGreyscale, + EditIcon, + ReplyIcon, + MoreIcon, + DeleteIcon, + PinIcon, + TypingIcon, + AddIcon +} from '../assets/icons'; +import PingSound from '../assets/sounds/ping.mp3'; +import CategorizedEmojis, { AllEmojis, getEmojiUrl } from '../assets/emojis'; +const fireIcon = getEmojiUrl('nature', 'fire'); +const heartIcon = getEmojiUrl('symbols', 'heart'); +const thumbsupIcon = getEmojiUrl('people', 'thumbsup'); +import GifPicker from './GifPicker'; // Cache for link metadata to prevent pop-in const metadataCache = new Map(); @@ -39,6 +59,11 @@ const LinkPreview = ({ url }) => { return () => { isMounted = false; }; }, [url]); + const [showControls, setShowControls] = useState(false); + const videoRef = useRef(null); + + // ... (fetchMeta logic) + // Helper to extract video ID const getYouTubeId = (link) => { const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; @@ -49,7 +74,69 @@ const LinkPreview = ({ url }) => { const videoId = getYouTubeId(url); const isYouTube = !!videoId; - if (loading || !metadata || (!metadata.title && !metadata.image)) return null; + if (loading || !metadata || (!metadata.title && !metadata.image && !metadata.video)) return null; + + // Special handling for direct video files (minimalist UI) + if (metadata.video && !isYouTube) { + const handlePlayClick = () => { + setShowControls(true); + if (videoRef.current) { + videoRef.current.play(); + } + }; + + return ( +
+
+ ); + } + + // Special handling for direct images/GIFs (minimalist UI) + if (metadata.description === 'Image File' && metadata.image) { + return ( +
isYouTube && setPlaying(true)} + > + Preview { + // Optional: Open Lightbox logic if needed + // But for now just display it nicely + }} + /> +
+ ); + } return (
@@ -89,15 +176,525 @@ const LinkPreview = ({ url }) => { ); }; -const ChatArea = ({ channelId, channelName, username }) => { +// Helper: Hex to Uint8Array +const fromHexString = (hexString) => + new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); + +// Helper: Buffer to Hex +const toHexString = (bytes) => + bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); + +const attachmentCache = new Map(); + +// Component to handle Attachment Rendering (Decrypt on load) +const Attachment = ({ metadata, onLoad, onImageClick }) => { + const [url, setUrl] = useState(attachmentCache.get(metadata.url) || null); + const [loading, setLoading] = useState(!attachmentCache.has(metadata.url)); + const [error, setError] = useState(null); + + // Video specific state + const [showControls, setShowControls] = useState(false); + const videoRef = useRef(null); + + useEffect(() => { + if (attachmentCache.has(metadata.url)) { + setUrl(attachmentCache.get(metadata.url)); + setLoading(false); + // Trigger scroll after render (microtask) + setTimeout(onLoad, 100); + return; + } + + let isMounted = true; + const decryptFile = async () => { + try { + // Fetch Encrypted Blob + const res = await fetch(metadata.url); + const blob = await res.blob(); + const arrayBuffer = await blob.arrayBuffer(); + + // Convert to Hex + const hexInput = toHexString(new Uint8Array(arrayBuffer)); + + // Safety Check + if (hexInput.length < 32) { + throw new Error('Invalid file data'); + } + + // Extract Tag (Last 16 bytes = 32 hex chars) + const TAG_HEX_LEN = 32; + const contentHex = hexInput.slice(0, -TAG_HEX_LEN); + const tagHex = hexInput.slice(-TAG_HEX_LEN); + + const decrypted = await window.cryptoAPI.decryptData( + contentHex, + metadata.key, + metadata.iv, + tagHex, + { encoding: 'buffer' } + ); + + const decryptedBlob = new Blob([decrypted], { type: metadata.mimeType }); + const objectUrl = URL.createObjectURL(decryptedBlob); + + if (isMounted) { + attachmentCache.set(metadata.url, objectUrl); + setUrl(objectUrl); + setLoading(false); + // Trigger scroll after decryption + setTimeout(onLoad, 100); + } + } catch (err) { + console.error('Attachment decrypt error:', err); + if (isMounted) { + setError('Failed to decrypt'); + setLoading(false); + } + } + }; + decryptFile(); + return () => { isMounted = false; }; + }, [metadata, onLoad]); + + if (loading) return
Downloading & Decrypting...
; + if (error) return
{error}
; + + if (metadata.mimeType.startsWith('image/')) { + return ( + {metadata.filename} onImageClick(url)} + /> + ); + } + if (metadata.mimeType.startsWith('video/')) { + const handlePlayClick = () => { + setShowControls(true); + if (videoRef.current) { + videoRef.current.play(); + } + }; + + return ( +
+
+ ); + } + + return ( +
+ 📄 +
+
{metadata.filename}
+
{(metadata.size / 1024).toFixed(1)} KB
+ Download +
+
+ ); +}; + +// Component for Pending File Preview +const PendingFilePreview = ({ file, onRemove }) => { + const [preview, setPreview] = useState(null); + + useEffect(() => { + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onloadend = () => setPreview(reader.result); + reader.readAsDataURL(file); + } else if (file.type.startsWith('video/')) { + // For video, we could try to generate a thumbnail, but for now just use an icon + setPreview(null); + } + }, [file]); + + return ( +
+ {preview ? ( + Preview + ) : ( +
+
📄
+
{file.name}
+
+ )} +
onRemove(file)} + style={{ + position: 'absolute', + top: '4px', + right: '4px', + backgroundColor: '#ed4245', + color: 'white', + borderRadius: '50%', + width: '20px', + height: '20px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + fontSize: '12px', + fontWeight: 'bold', + boxShadow: '0 2px 4px rgba(0,0,0,0.5)' + }} + > + ✕ +
+
+ ); +}; + +// Component for Drag Overlay +const DragOverlay = () => ( +
+
+ + + +
+
Upload to #{'channel'}
+
Hold Shift to upload directly
+
+); + +const EmojiButton = ({ onClick, active }) => { + const [hovered, setHovered] = useState(false); + const [bgPos, setBgPos] = useState('0px 0px'); + + const getRandomPos = () => { + // Sheet: 960x192, 20x4, 48px sprites. + // Target: 24px sprites (50% scale). + // Total Valid: 20*4 - 3 = 77. + const totalSprites = 77; + const index = Math.floor(Math.random() * totalSprites); + const col = index % 20; + const row = Math.floor(index / 20); + return `-${col * 24}px -${row * 24}px`; + }; + + const handleMouseEnter = () => { + setHovered(true); + setBgPos(getRandomPos()); + }; + + const handleMouseLeave = () => { + setHovered(false); + setBgPos(getRandomPos()); + }; + + return ( +
{ e.stopPropagation(); if(onClick) onClick(); }} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + style={{ + width: '24px', + height: '24px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginLeft: '4px' + }} + title="Select Emoji" + > +
+
+ ); +}; + +const MessageToolbar = ({ onAddReaction, onEdit, onReply, onMore, isOwner }) => { + return ( +
+ onAddReaction('thumbsup')} + title="Add Reaction" + emoji={} + /> + + onAddReaction('heart')} + title="Add Reaction" + emoji={} + /> + onAddReaction('fire')} + title="Add Reaction" + emoji={} + /> +
+ +
+ onAddReaction(null)} + title="Add Reaction" + emoji={} + /> + {isOwner && ( + } + /> + )} + } + /> + } + /> +
+ ); +}; + +const IconButton = ({ onClick, title, emoji }) => ( +
{ e.stopPropagation(); onClick(e); }} + title={title} + style={{ + cursor: 'pointer', + padding: '6px', + fontSize: '16px', + lineHeight: 1, + color: '#b9bbbe', + transition: 'background-color 0.1s' + }} + onMouseEnter={(e) => e.target.style.backgroundColor = '#40444b'} + onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'} + > + {emoji} +
+); + +const MessageContextMenu = ({ x, y, onInteract, onClose, isOwner }) => { + const menuRef = useRef(null); + const [pos, setPos] = useState({ top: y, left: x }); + + useEffect(() => { + const handleClickOutside = () => onClose(); + window.addEventListener('click', handleClickOutside); + return () => window.removeEventListener('click', handleClickOutside); + }, [onClose]); + + // Adjust position to prevent overflow + React.useLayoutEffect(() => { + if (menuRef.current) { + const rect = menuRef.current.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let newTop = y; + let newLeft = x; + + // Flip horizontally if overflow right + if (x + rect.width > viewportWidth) { + newLeft = x - rect.width; + } + + // Flip vertically if overflow bottom + if (y + rect.height > viewportHeight) { + newTop = y - rect.height; + } + + // Safety clamp (never negative) + if (newLeft < 0) newLeft = 10; + if (newTop < 0) newTop = 10; + + setPos({ top: newTop, left: newLeft }); + } + }, [x, y]); + + const MenuItem = ({ label, iconSrc, iconColor, onClick, danger }) => ( +
{ e.stopPropagation(); onClick(); onClose(); }} + style={{ + display: 'flex', + alignItems: 'center', + padding: '10px 12px', + cursor: 'pointer', + fontSize: '14px', + color: danger ? 'color-mix(in oklab, hsl(1.353 calc(1*82.609%) 68.431% /1) 100%, #000 0%)' : '#dcddde', + justifyContent: 'space-between', + whiteSpace: 'nowrap' + }} + onMouseEnter={(e) => e.currentTarget.style.backgroundColor = danger ? 'color-mix(in oklab,hsl(355.636 calc(1*64.706%) 50% /0.0784313725490196) 100%,hsl(0 0% 0% /0.0784313725490196) 0%)' : 'hsl(240 calc(1*4%) 60.784% /0.0784313725490196)'} + onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} + > + {label} +
+ +
+
+ ); + + return ( +
e.stopPropagation()} + > + onInteract('reaction')} /> + {isOwner && onInteract('edit')} />} + onInteract('reply')} /> +
+ onInteract('pin')} /> +
+ {isOwner && onInteract('delete')} />} +
+ ); +}; + +const ColoredIcon = ({ src, color, size = '24px', style = {} }) => ( +
+ +
+); + +const ChatArea = ({ channelId, channelName, username, channelKey, userId: currentUserId }) => { const [messages, setMessages] = useState([]); - const [input, setInput] = useState(''); + const [input, setInput] = useState(''); // Keeps track of text content for logic const [socket, setSocket] = useState(null); const messagesEndRef = useRef(null); - const textareaRef = useRef(null); + const messagesContainerRef = useRef(null); + const inputDivRef = useRef(null); // Ref for contentEditable + const [zoomedImage, setZoomedImage] = useState(null); + const [showGifPicker, setShowGifPicker] = useState(false); + const [pickerActiveTab, setPickerActiveTab] = useState('GIFs'); // Lifted active tab state + const savedRangeRef = useRef(null); // Track cursor position + const [isDragging, setIsDragging] = useState(false); + const [pendingFiles, setPendingFiles] = useState([]); + const [hasImages, setHasImages] = useState(false); // Track if input has standard emoji images + const [isMultiline, setIsMultiline] = useState(false); // Track if input is multiline + const [hoveredMessageId, setHoveredMessageId] = useState(null); + const [contextMenu, setContextMenu] = useState(null); // { x, y, messageId } + const [typingUsers, setTypingUsers] = useState(new Set()); + const typingTimeoutRef = useRef(null); + const lastTypingEmitRef = useRef(0); + // Reactions: { [msgId]: { [emoji]: { count: number, me: boolean, users: [] } } } + const [reactions, setReactions] = useState({}); - // Mock Key for demo (32 bytes hex = 64 chars) - const DEMO_CHANNEL_KEY = '000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f'; + // Close GIF picker when clicking outside + useEffect(() => { + const handleClickOutside = () => { + if (showGifPicker) setShowGifPicker(false); + }; + window.addEventListener('click', handleClickOutside); + return () => window.removeEventListener('click', handleClickOutside); + }, [showGifPicker]); + + const ICON_COLOR_DEFAULT = 'color-mix(in oklab, hsl(240 4.294% 68.039% / 1) 100%, #000 0%)'; // Helper to get consistent color for user const getUserColor = (username) => { @@ -117,10 +714,14 @@ const ChatArea = ({ channelId, channelName, username }) => { return '[Invalid Encrypted Message]'; } + if (!channelKey) { + return '[Encrypted Message - Key Missing]'; + } + const tag = msg.ciphertext.slice(-TAG_LENGTH); const content = msg.ciphertext.slice(0, -TAG_LENGTH); - const decrypted = await window.cryptoAPI.decryptData(content, DEMO_CHANNEL_KEY, msg.nonce, tag); + const decrypted = await window.cryptoAPI.decryptData(content, channelKey, msg.nonce, tag); return decrypted; } catch (e) { console.error('Decryption failed for msg:', msg.id, e); @@ -151,10 +752,11 @@ const ChatArea = ({ channelId, channelName, username }) => { }; useEffect(() => { + setMessages([]); // Clear messages immediately to prevent ghosting const newSocket = io('http://localhost:3000'); setSocket(newSocket); - newSocket.emit('join_channel', channelId); + newSocket.emit('join_channel', { channelId, userId: currentUserId }); newSocket.on('recent_messages', async (msgs) => { const processedMessages = await Promise.all(msgs.map(async (msg) => { @@ -162,6 +764,16 @@ const ChatArea = ({ channelId, channelName, username }) => { const isVerified = await verifyMessage(msg); return { ...msg, content, isVerified }; })); + + // Extract internal reactions from query result + const initialReactions = {}; + processedMessages.forEach(msg => { + if (msg.reactions) { + initialReactions[msg.id] = msg.reactions; + } + }); + setReactions(initialReactions); + setMessages(processedMessages); }); @@ -169,40 +781,328 @@ const ChatArea = ({ channelId, channelName, username }) => { const content = await decryptMessage(msg); const isVerified = await verifyMessage(msg); setMessages(prev => [...prev, { ...msg, content, isVerified }]); + + // Play Ping if mentioned + if (content && content.includes(`@${username}`)) { + try { + const audio = new Audio(PingSound); + audio.volume = 0.5; // Reasonable volume + await audio.play(); + } catch (err) { + console.error("Audio play failed", err); + } + } + }); + + + + // Reaction Listeners + newSocket.on('reaction_added', ({ messageId, userId, emoji }) => { + setReactions(prev => { + const msgReactions = prev[messageId] || {}; + const emojiData = msgReactions[emoji] || { count: 0, me: false }; + + return { + ...prev, + [messageId]: { + ...msgReactions, + [emoji]: { + count: emojiData.count + 1, + me: userId === currentUserId ? true : emojiData.me + } + } + }; + }); + }); + + newSocket.on('reaction_removed', ({ messageId, userId, emoji }) => { + setReactions(prev => { + const msgReactions = prev[messageId] || {}; + const emojiData = msgReactions[emoji]; + if (!emojiData) return prev; + + const newCount = emojiData.count - 1; + if (newCount <= 0) { + const { [emoji]: _, ...rest } = msgReactions; + return { ...prev, [messageId]: rest }; + } + + return { + ...prev, + [messageId]: { + ...msgReactions, + [emoji]: { + count: newCount, + me: userId === currentUserId ? false : emojiData.me + } + } + }; + }); + }); + + // Typing Listeners + newSocket.on('typing_start', ({ username: user, channelId: cId }) => { + if (cId !== channelId || user === username) return; + setTypingUsers(prev => { + const next = new Set(prev); + next.add(user); + return next; + }); + // Auto-clear safety backup (6s) + setTimeout(() => { + setTypingUsers(prev => { + const next = new Set(prev); + next.delete(user); + return next; + }); + }, 6000); + }); + + newSocket.on('typing_stop', ({ username: user, channelId: cId }) => { + if (cId !== channelId) return; + setTypingUsers(prev => { + const next = new Set(prev); + next.delete(user); + return next; + }); }); return () => newSocket.close(); - }, [channelId]); + }, [channelId, channelKey]); // Re-run if key changes - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); - - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + // ... (rest of the file) + const scrollToBottom = (force = false) => { + if (force) { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + return; } - }, [input]); - const handleSend = async (e) => { + // Smart Scroll: Only scroll if already near bottom (e.g., within 200px) + const container = messagesContainerRef.current; + if (container) { + const { scrollTop, scrollHeight, clientHeight } = container; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 300; // Threshold + if (isNearBottom) { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + } else { + // Fallback if ref not set yet + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }; + + // Auto-scroll on new messages + useEffect(() => { + if (messages.length > 0) { + const lastMsg = messages[messages.length - 1]; + // Always scroll if WE sent it, otherwise smart scroll + const isMyMessage = lastMsg.username === username; + scrollToBottom(isMyMessage); + } + }, [messages, username]); + + const fileInputRef = useRef(null); + // Helper to insert emoji at caret + const insertEmoji = (emoji) => { + if (!inputDivRef.current) return; + + const img = document.createElement('img'); + img.src = emoji.src; + img.alt = `:${emoji.name}:`; + img.className = "inline-emoji"; + img.style.width = "22px"; + img.style.height = "22px"; + img.style.verticalAlign = "bottom"; + img.style.margin = "0 1px"; + img.contentEditable = "false"; + + const space = document.createTextNode(' '); + + inputDivRef.current.focus(); + + // Restore saved range if valid + const sel = window.getSelection(); + if (savedRangeRef.current && inputDivRef.current.contains(savedRangeRef.current.commonAncestorContainer)) { + sel.removeAllRanges(); + sel.addRange(savedRangeRef.current); + } + + if (sel.rangeCount > 0 && inputDivRef.current.contains(sel.getRangeAt(0).commonAncestorContainer)) { + const range = sel.getRangeAt(0); + range.deleteContents(); + + // Range.insertNode inserts at the start of the range. + // To get [img][space], we insert space first (pushed to right), then img (at start). + range.insertNode(space); + range.insertNode(img); + + // Move caret to after the space + range.setStartAfter(space); + range.collapse(true); + + sel.removeAllRanges(); + sel.addRange(range); + } else { + inputDivRef.current.appendChild(img); + inputDivRef.current.appendChild(space); + + // Move caret to end + const range = document.createRange(); + range.setStartAfter(space); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + } + + setInput(inputDivRef.current.textContent); // Update state trigger + setHasImages(true); // Definitely has images now + }; + + // Check for typed emoji codes :fire: + const checkTypedEmoji = () => { + if (!inputDivRef.current) return; + + const text = inputDivRef.current.innerText; // InnerText preserves newlines better? + // Simple regex: look for :name: that matches a known emoji + // This is tricky in contentEditable nodes. + // A simple approach is replacing the TEXT node content if it ends with :name: + + // Advanced: Use TreeWalker to find text nodes containing the pattern + // Implementation complexity is high for reliable replacement while typing. + // For now, let's stick to Picker insertion. + // User asked: "enter for example :fire: it will automatically replace it" + + const selection = window.getSelection(); + if (!selection.rangeCount) return; + const range = selection.getRangeAt(0); + const node = range.startContainer; + + if (node.nodeType === Node.TEXT_NODE) { + const content = node.textContent.substring(0, range.startOffset); + const match = content.match(/:([a-zA-Z0-9_]+):$/); + if (match) { + const name = match[1]; + const emoji = AllEmojis.find(e => e.name === name); + if (emoji) { + // Replace! + const img = document.createElement('img'); + img.src = emoji.src; + img.alt = `:${name}:`; + img.className = "inline-emoji"; + img.style.width = "22px"; + img.style.height = "22px"; + img.style.verticalAlign = "bottom"; + img.style.margin = "0 1px"; + img.contentEditable = "false"; + + // We need to split the text node + const textBefore = node.textContent.substring(0, range.startOffset - match[0].length); + const textAfter = node.textContent.substring(range.startOffset); + + node.textContent = textBefore; + const afterNode = document.createTextNode(textAfter); + + if (node.nextSibling) { + node.parentNode.insertBefore(img, node.nextSibling); + node.parentNode.insertBefore(afterNode, img.nextSibling); + } else { + node.parentNode.appendChild(img); + node.parentNode.appendChild(afterNode); + } + + // Reset cursor + const newRange = document.createRange(); + newRange.setStart(afterNode, 0); + newRange.collapse(true); + selection.removeAllRanges(); + selection.addRange(newRange); + } + } + } + }; + const [uploading, setUploading] = useState(false); + + // ... (existing constants) + + const processFile = (file) => { + setPendingFiles(prev => [...prev, file]); + }; + + const handleFileSelect = (e) => { + if (e.target.files && e.target.files.length > 0) { + Array.from(e.target.files).forEach(processFile); + } + }; + + const handleDragOver = (e) => { e.preventDefault(); - if (!input.trim()) return; + e.stopPropagation(); + if (!isDragging) setIsDragging(true); + }; + const handleDragLeave = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.currentTarget.contains(e.relatedTarget)) return; + setIsDragging(false); + }; + + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + Array.from(e.dataTransfer.files).forEach(processFile); + } + }; + + const uploadAndSendFile = async (file) => { + const fileKey = await window.cryptoAPI.randomBytes(32); // 256-bit + const arrayBuffer = await file.arrayBuffer(); + const fileBytes = new Uint8Array(arrayBuffer); + const encrypted = await window.cryptoAPI.encryptData(fileBytes, fileKey); + const encryptedHex = encrypted.content + encrypted.tag; + const encryptedBytes = fromHexString(encryptedHex); + const blob = new Blob([encryptedBytes], { type: 'application/octet-stream' }); + + const formData = new FormData(); + formData.append('file', blob, file.name + '.enc'); + + const res = await fetch('http://localhost:3000/api/upload', { + method: 'POST', + body: formData + }); + const data = await res.json(); + if (!data.url) throw new Error('Upload failed'); + + const metadata = { + type: 'attachment', + url: `http://localhost:3000${data.url}`, + filename: file.name, + mimeType: file.type, + size: file.size, + key: fileKey, + iv: encrypted.iv + }; + + await sendMessage(JSON.stringify(metadata)); + }; + + const sendMessage = async (contentString) => { try { - const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(input, DEMO_CHANNEL_KEY); + if (!channelKey) { + alert("Cannot send: Missing Encryption Key"); + return; + } + + const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(contentString, channelKey); const ciphertext = encryptedContent + tag; const senderId = localStorage.getItem('userId'); const signingKey = sessionStorage.getItem('signingKey'); - if (!senderId) { - console.error('No userId found in localStorage'); + if (!senderId || !signingKey) { return; } - if (!signingKey) { - console.error('No signingKey found in sessionStorage. Please relogin.'); - return; // Prevent sending unsigned messages - } const messageData = { channelId, @@ -214,42 +1114,195 @@ const ChatArea = ({ channelId, channelName, username }) => { }; socket.emit('send_message', messageData); - setInput(''); } catch (err) { console.error('Send error:', err); } }; + const handleSend = async (e) => { + e.preventDefault(); + + // Construct message from Rich Text + let messageContent = ''; + if (inputDivRef.current) { + inputDivRef.current.childNodes.forEach(node => { + if (node.nodeType === Node.TEXT_NODE) { + messageContent += node.textContent; + } else if (node.nodeName === 'IMG' && node.alt) { + messageContent += node.alt; + } else if (node.tagName === 'DIV' || node.tagName === 'BR') { + messageContent += '\n'; // Basic newline handling + } else { + messageContent += node.textContent; + } + }); + // Cleanup extra newlines if recursive div usage (Chrome does

for lines) + messageContent = messageContent.trim(); + } + + if (!messageContent && pendingFiles.length === 0) return; + + setUploading(true); + try { + // Process Pending Files + for (const file of pendingFiles) { + await uploadAndSendFile(file); + } + // Clear pending files + setPendingFiles([]); + + // Send Text Message if any + if (messageContent) { + await sendMessage(messageContent); + // Clear Input + if (inputDivRef.current) inputDivRef.current.innerHTML = ''; + setInput(''); + setHasImages(false); // Clear image flag + + // Stop typing immediately + if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); + socket?.emit('typing_stop', { channelId, username }); + lastTypingEmitRef.current = 0; + } + } catch (err) { + console.error("Error sending message/files:", err); + alert("Failed to send message/files"); + } finally { + setUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(e); } + + // Handle Backspace for inline emojis (contentEditable="false") + if (e.key === 'Backspace' && inputDivRef.current) { + const sel = window.getSelection(); + if (sel.rangeCount > 0 && sel.isCollapsed) { + const range = sel.getRangeAt(0); + + // Check if we are right after an emoji + // Logic: Look at startContainer/startOffset + // If startContainer is the inputDiv or a text node inside it. + // Simplified check: Use a range to verify logical previous sibling + + // If preceding char is our zero-width space or just verifying previous element + if (range.startOffset === 0 && range.startContainer !== inputDivRef.current) { + // Start of a text node? check previous sibling of that node + const prevNode = range.startContainer.previousSibling; + if (prevNode && prevNode.nodeName === 'IMG' && prevNode.classList.contains('inline-emoji')) { + e.preventDefault(); + prevNode.remove(); + setHasImages(inputDivRef.current.querySelectorAll('img').length > 0); + return; // Done + } + } else if (range.startOffset > 0) { + // Check range.startContainer.childNodes[offset-1] if container is Element + if (range.startContainer.nodeType === Node.ELEMENT_NODE) { + const nodeBefore = range.startContainer.childNodes[range.startOffset - 1]; + if (nodeBefore && nodeBefore.nodeName === 'IMG' && nodeBefore.classList.contains('inline-emoji')) { + e.preventDefault(); + nodeBefore.remove(); + setHasImages(inputDivRef.current.querySelectorAll('img').length > 0); + return; + } + } + // If container is text node, we don't delete IMG inside it (not possible), + // but maybe we are at offset 1 and char at 0 is space after img? + } + + // What if we just deleted the space after the emoji? Cursor is now right after emoji? + // The browser might handle this awkwardly. + // Let's rely on standard deletion for the space, and then this catches the emoji. + } + } + }; + + // Helper to format mentions + const formatMentions = (text) => { + if (!text) return ''; + return text.replace(/@(\w+)/g, '[@$1](mention://$1)'); }; return ( -
-
+
+ {isDragging && } + +
{messages.map((msg, idx) => { - const urls = extractUrls(msg.content); + // ... date logic ... const currentDate = new Date(msg.created_at); const previousDate = idx > 0 ? new Date(messages[idx - 1].created_at) : null; - const isNewDay = !previousDate || ( currentDate.getDate() !== previousDate.getDate() || currentDate.getMonth() !== previousDate.getMonth() || currentDate.getFullYear() !== previousDate.getFullYear() ); + // Try parsing content as JSON (Metadata) + let isAttachment = false; + let attachmentMetadata = null; + try { + if (msg.content.startsWith('{')) { + const parsed = JSON.parse(msg.content); + if (parsed.type === 'attachment') { + isAttachment = true; + attachmentMetadata = parsed; + } + } + } catch (e) {} + + const isMentioned = msg.content && msg.content.includes(`@${username}`); + return ( - + {isNewDay && (
{currentDate.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}
)} -
+
setHoveredMessageId(msg.id)} + onMouseLeave={() => setHoveredMessageId(null)} + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu({ + x: e.clientX, + y: e.clientY, + messageId: msg.id, + isOwner: msg.username === username + }); + }} + > + {isMentioned && ( +
+ )}
{ {currentDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
-
- ( - { - e.preventDefault(); - window.cryptoAPI.openExternal(props.href); - }} - style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }} - onMouseOver={(e) => e.target.style.textDecoration = 'underline'} - onMouseOut={(e) => e.target.style.textDecoration = 'none'} - /> - ), - code({ node, inline, className, children, ...props }) { - const match = /language-(\w+)/.exec(className || '') - return !inline && match ? ( - +
+ {isAttachment ? ( + + ) : ( + <> + {(() => { + const urls = extractUrls(msg.content); + const isOnlyUrl = urls.length === 1 && msg.content.trim() === urls[0]; + const isGif = isOnlyUrl && (urls[0].includes('tenor.com') || urls[0].includes('giphy.com') || urls[0].endsWith('.gif')); + + if (isGif) return null; + + return ( + url} // Allow Custom Schemas like mention:// + components={{ + a: ({ node, ...props }) => { + if (props.href && props.href.startsWith('mention://')) { + return ( + + {props.children} + + ); + } + return ( + { + e.preventDefault(); + window.cryptoAPI.openExternal(props.href); + }} + style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }} + onMouseOver={(e) => e.target.style.textDecoration = 'underline'} + onMouseOut={(e) => e.target.style.textDecoration = 'none'} + /> + ); + }, + code({ node, inline, className, children, ...props }) { + const match = /language-(\w+)/.exec(className || '') + return !inline && match ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ) + }, + p: ({ node, ...props }) =>

, + h1: ({ node, ...props }) =>

, + h2: ({ node, ...props }) =>

, + h3: ({ node, ...props }) =>

, + ul: ({ node, ...props }) =>
    , + ol: ({ node, ...props }) =>
      , + li: ({ node, ...props }) =>
    1. , + hr: ({ node, ...props }) =>
      , + }} > - {String(children).replace(/\n$/, '')} - - ) : ( - - {children} - - ) - }, - p: ({ node, ...props }) =>

      , - h1: ({ node, ...props }) =>

      , - h2: ({ node, ...props }) =>

      , - h3: ({ node, ...props }) =>

      , - ul: ({ node, ...props }) =>
        , - ol: ({ node, ...props }) =>
          , - li: ({ node, ...props }) =>
        1. , - hr: ({ node, ...props }) =>
          , - }} - > - {msg.content} - - {urls.map((url, i) => ( - - ))} + {formatMentions(msg.content)} + + ); + })()} + {extractUrls(msg.content).map((url, i) => ( + + ))} + + )} + {/* Reactions Grid */} + {reactions[msg.id] && Object.keys(reactions[msg.id]).length > 0 && ( +
          + {Object.entries(reactions[msg.id]).map(([emojiName, data]) => { + const getIcon = (name) => { + switch(name) { + case 'thumbsup': return thumbsupIcon; + case 'heart': return heartIcon; + case 'fire': return fireIcon; + default: return heartIcon; // Fallback or handle custom + } + }; + return ( +
          { + // Toggle logic + if (data.me) { + socket.emit('remove_reaction', { channelId, messageId: msg.id, userId: currentUserId, emoji: emojiName }); + } else { + socket.emit('add_reaction', { channelId, messageId: msg.id, userId: currentUserId, emoji: emojiName }); + } + }} + style={{ + display: 'flex', + alignItems: 'center', + backgroundColor: data.me ? 'rgba(88, 101, 242, 0.15)' : '#2f3136', + border: data.me ? '1px solid #5865F2' : '1px solid transparent', + borderRadius: '8px', + padding: '2px 6px', + cursor: 'pointer', + gap: '4px' + }} + > + + {data.count} +
          + ); + })} +
          + )} +

+ {hoveredMessageId === msg.id && ( + { + const emojiName = emoji || 'heart'; + socket.emit('add_reaction', { channelId, messageId: msg.id, userId: currentUserId, emoji: emojiName }); + }} + onEdit={() => console.log('Edit', msg.id)} + onReply={() => console.log('Reply', msg.id)} + onMore={(e) => { + const rect = e.target.getBoundingClientRect(); + setContextMenu({ x: rect.left, y: rect.bottom, messageId: msg.id, isOwner: msg.username === username }); + }} + /> + )}
@@ -331,39 +1479,253 @@ const ChatArea = ({ channelId, channelName, username }) => {
- -
- -