From 0fefb3d05233028cec5efc55f92573a02ab1daab Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Fri, 19 Jun 2026 00:13:20 -0500 Subject: [PATCH 01/13] chat feature with email recog added --- .DS_Store | Bin 0 -> 6148 bytes bsg-frontend/.DS_Store | Bin 0 -> 6148 bytes bsg-frontend/apps/.DS_Store | Bin 0 -> 6148 bytes .../RoomDisplays/ChatDisplay.tsx | 3 +- .../apps/extension/hooks/useChatSocket.ts | 2 +- .../apps/extension/out/defaultPopup.html | 2 +- bsg-frontend/package-lock.json | 117 ++++++++++++------ server/package-lock.json | 5 - 8 files changed, 82 insertions(+), 47 deletions(-) create mode 100644 .DS_Store create mode 100644 bsg-frontend/.DS_Store create mode 100644 bsg-frontend/apps/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6d5983ca4a1f5f80d76632afaab3f12903ae9d63 GIT binary patch literal 6148 zcmeHKyG{c!5S)b+kw}q}(mz1KADp7(3-|#Pj?kc-L`Z)X-^Hgf`)E!FU1*|NX+8FO z$CjtKy#-*KbN>je0W9f``0`n~E5b&NB}9zc9G_Bi1U4-@_o z{h>d-?1$mqlVnmr3P=GdAO)nrMHHyAy1cu{X*xs-NP%loz`qZT?${NMiSg;+5G??4 z!EhMo(Mu4U2Z&wan8*mtl1fafRf}OsXS`KjS2!jn9ahbU)y-BNipA|bzePH%OVlU@ zq`*{x+gz@_{_p62^#4H1@V-^m;4I%_5-~#-K2??Z%JxAx+@j_rm6?#VYi=A4u-_X<| zqMPSsEi#Bm4>y&Sg~k+lCr5eUH(oE}`EJ);>!>lNF#jv{Xd}rZGKew5pFG zR`+&jitBP{sao4bWBAbcXSFE?rnOx(A%SUjVITn#Xb8+}KG^xcgMXX-s>Aexkv`wRTRDoVb9A3%^GA)#CmkSMRpcjeO;GMck3P^$LP{6+rjqcbB$He$_FvJKzoG~57 zb<7gP<_Tgi921$LSyG8fwHh%j>CCsP>xE-t(qT1xSUuTlLa}%{?{85K>xqg|Knk2H zaGTqO_y0Bh%=~{!(oPCUfvZx$W~-;wlCM;~b@FoFYa9KU?lnic8`nW$h;~ejcFc{p e { } = useChatSocket(); const username = useUserStore(s => s.username); + const email = useUserStore(s => s.email); // ← add this line const emojiMap = require('@bsg/ui-styles/assets/emojis.json') as Record; const emojiList = Object.entries(emojiMap).flatMap(([category, emojis]) => emojis.map(emoji => ({ ...emoji, category })) @@ -76,7 +77,7 @@ export const ChatDisplay = ({ isActive }: { isActive: boolean }) => { : <> - {(group[0].userName === username) ? + {(group[0].userHandle === email) ? // just changed // User's own message
{ if ( lastGroup && !msg.isSystem && - lastGroup[0].userName === msg.userName + lastGroup[0].userHandle === msg.userHandle //lastGroup[0].userName === msg.userName ) { lastGroup.push(msg); } else { diff --git a/bsg-frontend/apps/extension/out/defaultPopup.html b/bsg-frontend/apps/extension/out/defaultPopup.html index d7d5986a..4c44d20c 100644 --- a/bsg-frontend/apps/extension/out/defaultPopup.html +++ b/bsg-frontend/apps/extension/out/defaultPopup.html @@ -1 +1 @@ -

BSG_

You are not on LeetCode. Once you go to the website you can open up the side panel to start solving!

\ No newline at end of file +

BSG_

You are not on LeetCode. Once you go to the website you can open up the side panel to start solving!

\ No newline at end of file diff --git a/bsg-frontend/package-lock.json b/bsg-frontend/package-lock.json index 2c5063bd..c50192f7 100644 --- a/bsg-frontend/package-lock.json +++ b/bsg-frontend/package-lock.json @@ -199,6 +199,10 @@ "node": ">=6.9.0" } }, + "node_modules/@bsg/assets": { + "resolved": "packages/assets", + "link": true + }, "node_modules/@bsg/components": { "resolved": "packages/components/customComponents", "link": true @@ -311,7 +315,6 @@ "version": "6.36.5", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.5.tgz", "integrity": "sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "style-mod": "^4.1.0", @@ -465,7 +468,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.18", "@firebase/logger": "0.4.4", @@ -532,7 +534,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.13.2", "@firebase/component": "0.6.18", @@ -548,8 +549,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth-compat": { "version": "0.5.28", @@ -1000,7 +1000,6 @@ "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -1066,7 +1065,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -1781,6 +1779,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -3957,6 +3956,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3967,6 +3967,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -3976,7 +3977,8 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/filesystem": { "version": "0.0.36", @@ -4003,7 +4005,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -4025,7 +4028,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4035,7 +4037,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4447,6 +4448,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -4456,25 +4458,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4485,13 +4491,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4504,6 +4512,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -4513,6 +4522,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4521,13 +4531,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4544,6 +4556,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4557,6 +4570,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4569,6 +4583,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4583,6 +4598,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4592,19 +4608,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4640,6 +4657,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4657,6 +4675,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4672,7 +4691,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "peer": true }, "node_modules/ansi-regex": { "version": "5.0.1", @@ -5052,7 +5072,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5214,6 +5233,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, + "peer": true, "engines": { "node": ">=6.0" } @@ -5841,7 +5861,8 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -5929,7 +5950,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6083,7 +6103,6 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -6335,6 +6354,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "peer": true, "engines": { "node": ">=0.8.x" } @@ -6479,7 +6499,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "peer": true }, "node_modules/fastq": { "version": "1.19.1", @@ -7533,6 +7554,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7547,6 +7569,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7681,6 +7704,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, + "peer": true, "engines": { "node": ">=6.11.5" } @@ -7752,7 +7776,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/merge2": { "version": "1.4.1", @@ -7779,6 +7804,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -7788,6 +7814,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7877,7 +7904,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/next": { "version": "16.2.2", @@ -8799,6 +8827,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -8808,7 +8837,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8818,7 +8846,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8836,7 +8863,6 @@ "version": "7.55.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz", "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -8851,15 +8877,13 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "peer": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9018,8 +9042,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -9164,6 +9187,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9329,6 +9353,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9365,6 +9390,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -9376,7 +9402,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "peer": true }, "node_modules/semver": { "version": "7.7.3", @@ -9395,6 +9422,7 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -9627,6 +9655,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9644,6 +9673,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10111,6 +10141,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10129,6 +10160,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -10162,7 +10194,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/text-table": { "version": "0.2.0", @@ -10227,7 +10260,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10374,7 +10406,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10607,6 +10638,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, + "peer": true, "engines": { "node": ">=10.13.0" } @@ -10616,6 +10648,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10629,6 +10662,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -10639,6 +10673,7 @@ "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -10900,6 +10935,10 @@ } } }, + "packages/assets": { + "name": "@bsg/assets", + "version": "1.0.0" + }, "packages/components/customComponents": { "name": "@bsg/components", "version": "1.0.0", diff --git a/server/package-lock.json b/server/package-lock.json index 8f880ef4..9e3b2aed 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -53,7 +53,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1067,7 +1066,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz", "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -1822,7 +1820,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2610,7 +2607,6 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", @@ -4778,7 +4774,6 @@ "resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz", "integrity": "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw==", "license": "MIT", - "peer": true, "dependencies": { "@redis/bloom": "5.10.0", "@redis/client": "5.10.0", From 353ee586b3386b1728ecc8c7a95a70c79215175a Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Sat, 20 Jun 2026 10:25:07 -0500 Subject: [PATCH 02/13] settings timer --- .../RoomDisplays/SettingsDisplay.tsx | 148 ++++++++++++++++++ .../customComponents/Toolbar/Toolbar.tsx | 3 +- .../apps/extension/hooks/useRoomInit.ts | 10 ++ .../apps/extension/out/defaultPopup.html | 2 +- .../apps/extension/pages/room-page.tsx | 2 + .../apps/extension/stores/useRoomStore.ts | 4 + bsg-frontend/packages/models/TabName.ts | 2 +- central-service/controllers/room.go | 31 ++++ central-service/services/room.go | 23 +++ central-service/services/round.go | 36 +++++ server/src/routes/rooms.js | 25 +++ 11 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx diff --git a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx new file mode 100644 index 00000000..c1c4c73f --- /dev/null +++ b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from 'react'; +import { useRoomStore } from '@/stores/useRoomStore'; +import { SERVER_URL } from '../../lib/config'; + +const MIN_DURATION = 5; +const MAX_DURATION = 120; +const STEP = 5; +const PRESETS = [15, 30, 45, 60]; + +export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { + + const roomId = useRoomStore(s => s.roomId); + const isAdmin = useRoomStore(s => s.isAdmin); + const isRoundStarted = useRoomStore(s => s.isRoundStarted); + const roundDuration = useRoomStore(s => s.roundDuration); + const setRoundDuration = useRoomStore(s => s.setRoundDuration); + + const [ value, setValue ] = useState(roundDuration ?? 30); + const [ saving, setSaving ] = useState(false); + const [ error, setError ] = useState(null); + const [ saved, setSaved ] = useState(false); + + // Sync the working value when the stored duration changes or the tab reopens. + useEffect(() => { + if (roundDuration != null) setValue(roundDuration); + }, [roundDuration, isActive]); + + const clamp = (n: number) => Math.max(MIN_DURATION, Math.min(MAX_DURATION, n)); + const change = (next: number) => { + setValue(clamp(next)); + setSaved(false); + setError(null); + }; + + const dirty = value !== roundDuration; + + const onSave = async () => { + if (!roomId) { + setError('Not in a room.'); + return; + } + setSaving(true); + setError(null); + try { + const res = await fetch(`${SERVER_URL}/rooms/${roomId}/rounds/time`, { + method: 'POST', + body: JSON.stringify({ duration: value }), + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + const data = await res.json().catch(() => null); + if (!res.ok) { + setError((data && (data.error || data.message)) || `Failed to update timer: ${res.status}`); + } else { + setRoundDuration(data?.data?.duration ?? value); + setSaved(true); + } + } catch (e: any) { + setError(e?.message || 'Failed to update timer.'); + } finally { + setSaving(false); + } + }; + + return ( +
+
Settings
+ +
+
Edit Timer
+ +
+ {!isAdmin ? ( +

+ Only the room admin can change the timer. +

+ ) : isRoundStarted ? ( +

+ The timer can only be edited before the round starts. +

+ ) : ( + <> +

+ Set how long the round will run. Applies when you start the round. +

+ +
+ +
+ {value} + min +
+ +
+ +
+ {PRESETS.map((p) => ( + + ))} +
+ + {error &&

{error}

} + {saved && !dirty && ( +

Timer updated.

+ )} + + + + )} +
+
+
+ ); +}; diff --git a/bsg-frontend/apps/extension/customComponents/Toolbar/Toolbar.tsx b/bsg-frontend/apps/extension/customComponents/Toolbar/Toolbar.tsx index b101cd08..56b25121 100644 --- a/bsg-frontend/apps/extension/customComponents/Toolbar/Toolbar.tsx +++ b/bsg-frontend/apps/extension/customComponents/Toolbar/Toolbar.tsx @@ -12,6 +12,7 @@ export const Toolbar = () => { const isAdmin = useRoomStore(s => s.isAdmin); const isRoundStarted = useRoomStore(s => s.isRoundStarted); + const setActiveTab = useRoomStore(s => s.setActiveTab); const { handleStartRound, handleEndRound, handleLeaveRoom } = useRoomEvents(); // TODO: Move timer into separate iframe @@ -114,7 +115,7 @@ export const Toolbar = () => {
\ No newline at end of file +

BSG_

You are not on LeetCode. Once you go to the website you can open up the side panel to start solving!

\ No newline at end of file diff --git a/bsg-frontend/apps/extension/pages/room-page.tsx b/bsg-frontend/apps/extension/pages/room-page.tsx index a7b9a4c3..2098575c 100644 --- a/bsg-frontend/apps/extension/pages/room-page.tsx +++ b/bsg-frontend/apps/extension/pages/room-page.tsx @@ -2,6 +2,7 @@ import { RoomInfoDisplay } from '@/customComponents/RoomDisplays/RoomInfoDisplay import { ChatDisplay } from '@/customComponents/RoomDisplays/ChatDisplay'; import { LeaderboardDisplay } from '@/customComponents/RoomDisplays/LeaderboardDisplay'; import { StatisticsDisplay } from '@/customComponents/RoomDisplays/StatisticsDisplay'; +import { SettingsDisplay } from '@/customComponents/RoomDisplays/SettingsDisplay'; import { useRoomStore } from '@/stores/useRoomStore'; export default function RoomPage() { @@ -30,6 +31,7 @@ export default function RoomPage() { + ); } \ No newline at end of file diff --git a/bsg-frontend/apps/extension/stores/useRoomStore.ts b/bsg-frontend/apps/extension/stores/useRoomStore.ts index 63ef69f8..ba9ae950 100644 --- a/bsg-frontend/apps/extension/stores/useRoomStore.ts +++ b/bsg-frontend/apps/extension/stores/useRoomStore.ts @@ -14,6 +14,7 @@ interface roomStoreState { activeTab: TabName; isRoundStarted: boolean; roundEndTime: number | null; + roundDuration: number | null; lastGameEvent: GameEvent | null; roomNotice: string | null; @@ -27,6 +28,7 @@ interface roomStoreState { setActiveTab: (activeTab: TabName) => void; setIsRoundStarted: (isRoundStarted: boolean) => void; setRoundEndTime: (roundEndTime: number | null) => void; + setRoundDuration: (roundDuration: number | null) => void; setLastGameEvent: (lastGameEvent: GameEvent | null) => void; setRoomNotice: (roomNotice: string | null) => void; @@ -50,6 +52,7 @@ const roomStoreInit = { activeTab: 'chat' as TabName, isRoundStarted: false, roundEndTime: null, + roundDuration: null, lastGameEvent: null, roomNotice: null, }; @@ -67,6 +70,7 @@ export const useRoomStore = create((set) => ({ setActiveTab: (activeTab) => set({ activeTab: activeTab }), setIsRoundStarted: (isRoundStarted) => set({ isRoundStarted: isRoundStarted }), setRoundEndTime: (roundEndTime) => set({ roundEndTime: roundEndTime }), + setRoundDuration: (roundDuration) => set({ roundDuration: roundDuration }), setLastGameEvent: (lastGameEvent) => set({ lastGameEvent: lastGameEvent }), setRoomNotice: (roomNotice) => set({ roomNotice: roomNotice }), diff --git a/bsg-frontend/packages/models/TabName.ts b/bsg-frontend/packages/models/TabName.ts index bb056ded..f655dbe8 100644 --- a/bsg-frontend/packages/models/TabName.ts +++ b/bsg-frontend/packages/models/TabName.ts @@ -1 +1 @@ -export type TabName = 'roomInfo' | 'chat' | 'leaderboard' | 'statistics'; \ No newline at end of file +export type TabName = 'roomInfo' | 'chat' | 'leaderboard' | 'statistics' | 'settings'; \ No newline at end of file diff --git a/central-service/controllers/room.go b/central-service/controllers/room.go index d00f6716..d97d86ff 100644 --- a/central-service/controllers/room.go +++ b/central-service/controllers/room.go @@ -165,6 +165,36 @@ func (controller *RoomController) StartRoundEndpoint(c echo.Context) error { }) } +func (controller *RoomController) UpdateRoundDurationEndpoint(c echo.Context) error { + targetRoomID := c.Param("roomID") + userAuthID := c.Get("userAuthID").(string) + var body struct { + Duration int `json:"duration"` + } + if err := c.Bind(&body); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid data. Please try again") + } + newDuration, err := controller.roomService.SetRoundDurationByRoomID(targetRoomID, userAuthID, body.Duration) + if err != nil { + controller.logger.Error("Failed to update round duration", err, map[string]interface{}{ + "room_id": targetRoomID, + "user_id": userAuthID, + }) + if bsgErr, ok := err.(services.BSGError); ok { + return echo.NewHTTPError(bsgErr.StatusCode, "Failed to update timer. "+bsgErr.Message) + } + if bsgErr, ok := err.(*services.BSGError); ok { + return echo.NewHTTPError(bsgErr.StatusCode, "Failed to update timer. "+bsgErr.Message) + } + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update timer. Please try again later") + } + return c.JSON(http.StatusOK, map[string]interface{}{ + "data": map[string]interface{}{ + "duration": newDuration, + }, + }) +} + func (controller *RoomController) GetLeaderboardEndpoint(c echo.Context) error { roomID := c.Param("roomID") leaderboard, err := controller.roomService.GetLeaderboard(roomID) @@ -256,6 +286,7 @@ func (controller *RoomController) InitializeRoutes(g *echo.Group) { g.POST("/:roomID/join", controller.JoinRoomEndpoint) g.POST("/:roomID/leave", controller.LeaveRoomEndpoint) g.POST("/:roomID/rounds/create", controller.CreateNewRoundEndpoint) + g.POST("/:roomID/rounds/time", controller.UpdateRoundDurationEndpoint) g.POST("/:roomID/start", controller.StartRoundEndpoint) g.POST("/:roomID/end", controller.EndRoundEndpoint) g.POST("/:roomID/:problemID", controller.CreateSubmissionEndpoint) diff --git a/central-service/services/room.go b/central-service/services/room.go index fcc33f90..45112a65 100644 --- a/central-service/services/room.go +++ b/central-service/services/room.go @@ -502,6 +502,29 @@ func (service *RoomService) StartRoundByRoomID(roomID string, userID string) (*t return roundStartTime, problems, nil } +// SetRoundDurationByRoomID updates the duration of the room's not-yet-started round. +// Reserved for the room admin, and only valid while a round is in the CREATED state. +func (service *RoomService) SetRoundDurationByRoomID(roomID string, userID string, duration int) (int, error) { + room, err := service.FindRoomByID(roomID) + if err != nil { + return 0, err + } + if room.Admin != userID { // check if user is room admin + return 0, BSGError{http.StatusUnauthorized, "User is not room admin. This functionality is reserved for room admin..."} + } + var round *models.Round + for i := range room.Rounds { + if room.Rounds[i].Status == constants.ROUND_CREATED { + round = &room.Rounds[i] + break + } + } + if round == nil { + return 0, BSGError{http.StatusNotFound, "No round to edit. The timer can only be changed before the round starts."} + } + return service.roundService.UpdateRoundDuration(round, duration) +} + func (service *RoomService) GetLeaderboard(roomID string) ([]redis.Z, error) { return service.roundService.GetLeaderboardByRoomID(roomID) } diff --git a/central-service/services/round.go b/central-service/services/round.go index b004ffe3..4706cc81 100644 --- a/central-service/services/round.go +++ b/central-service/services/round.go @@ -251,6 +251,42 @@ func (service *RoundService) InitiateRoundStart(round *models.Round, activeRoomP return &roundStartTime, round.ProblemSet, nil } +// Allowed round duration range (minutes), matching the create-round Slider in the UI. +const ( + MIN_ROUND_DURATION = 5 + MAX_ROUND_DURATION = 120 +) + +// UpdateRoundDuration sets the duration (in minutes) of a round that has not started yet. +// The timer only begins on round start, so before then this is a plain column update that +// the eventual InitiateRoundStart will read — nothing needs to be rescheduled or broadcast. +func (service *RoundService) UpdateRoundDuration(round *models.Round, newDuration int) (int, error) { + if round == nil { + return 0, &BSGError{ + Message: "Round not found", + StatusCode: 404, + } + } + if round.Status != constants.ROUND_CREATED { + return 0, &BSGError{ + Message: "Timer can only be edited before the round starts", + StatusCode: 400, + } + } + if newDuration < MIN_ROUND_DURATION || newDuration > MAX_ROUND_DURATION { + return 0, &BSGError{ + Message: fmt.Sprintf("Duration must be between %d and %d minutes", MIN_ROUND_DURATION, MAX_ROUND_DURATION), + StatusCode: 400, + } + } + // Explicit column update: GORM's Updates(struct) skips zero values, Update does not. + if err := service.db.Model(round).Update("duration", newDuration).Error; err != nil { + return 0, err + } + round.Duration = newDuration + return newDuration, nil +} + // Adds a user to a round leaderboard // Member values are a compression of the user's score and last submission timestamp // User's initial score will be 0 with the current timestamp diff --git a/server/src/routes/rooms.js b/server/src/routes/rooms.js index 4b01718d..083c66e0 100644 --- a/server/src/routes/rooms.js +++ b/server/src/routes/rooms.js @@ -71,6 +71,31 @@ router.post('/:id/rounds/create', ensureAuth, async (req, res) => { } }); +// Update Round Timer (pre-start duration edit) +router.post('/:id/rounds/time', ensureAuth, async (req, res) => { + const authID = req.user.id; + const { id } = req.params; + try { + const response = await fetch(`${centralServiceUrl}/api/rooms/${id}/rounds/time`, { + method: 'POST', + body: JSON.stringify(req.body), + headers: { + 'Content-Type': 'application/json', + 'X-Server-Secret': serverSecret, + 'X-User-Auth-ID': authID + } + }); + const data = await response.json(); + res.status(response.status).json(data); + } catch (error) { + logger.error('Error updating round timer', error, { + user_id: authID, + room_id: id, + }); + res.status(500).json({ error: 'Internal server error' }); + } +}); + // Start Round router.post('/:id/start', ensureAuth, async (req, res) => { const authID = req.user.id; From afb59813bb302c2df1169919f4b5efed2a48e5bf Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Sun, 28 Jun 2026 12:30:27 -0500 Subject: [PATCH 03/13] timer UI added Match settings-page timer to the create-room slider; stop tracking .DS_Store and build output via .gitignore. Co-Authored-By: Claude Opus 4.8 (1M context) --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 16 ++- bsg-frontend/.DS_Store | Bin 6148 -> 0 bytes bsg-frontend/apps/.DS_Store | Bin 6148 -> 0 bytes .../RoomDisplays/SettingsDisplay.tsx | 94 ++++++------------ .../apps/extension/out/defaultPopup.html | 1 - worker-service/.DS_Store | Bin 6148 -> 0 bytes 7 files changed, 46 insertions(+), 65 deletions(-) delete mode 100644 .DS_Store delete mode 100644 bsg-frontend/.DS_Store delete mode 100644 bsg-frontend/apps/.DS_Store delete mode 100644 bsg-frontend/apps/extension/out/defaultPopup.html delete mode 100644 worker-service/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 6d5983ca4a1f5f80d76632afaab3f12903ae9d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c!5S)b+kw}q}(mz1KADp7(3-|#Pj?kc-L`Z)X-^Hgf`)E!FU1*|NX+8FO z$CjtKy#-*KbN>je0W9f``0`n~E5b&NB}9zc9G_Bi1U4-@_o z{h>d-?1$mqlVnmr3P=GdAO)nrMHHyAy1cu{X*xs-NP%loz`qZT?${NMiSg;+5G??4 z!EhMo(Mu4U2Z&wan8*mtl1fafRf}OsXS`KjS2!jn9ahbU)y-BNipA|bzePH%OVlU@ zq`*{x+gz@_{_p62^#4H1@V-^m;4I%_5-~#-K2??Z%JxAx+@j_rm6?#VYi=A4u-_X<| zqMPSsEi#Bm4>y&Sg~k+lCr5eUH(oE}`EJ);>!>lNF#jv{Xd}rZGKew5pFG zR`+&jitBP{sao4bWBAbcXSFE?rnOx(A%SUjVITn#Xb8+}KG^xcgMXX-s>Aexkv`wRTRDoVb9A3%^GA)#CmkSMRpcjeO;GMck3P^$LP{6+rjqcbB$He$_FvJKzoG~57 zb<7gP<_Tgi921$LSyG8fwHh%j>CCsP>xE-t(qT1xSUuTlLa}%{?{85K>xqg|Knk2H zaGTqO_y0Bh%=~{!(oPCUfvZx$W~-;wlCM;~b@FoFYa9KU?lnic8`nW$h;~ejcFc{p e { @@ -63,7 +64,7 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { }; return ( -
+
Settings
@@ -79,70 +80,37 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { The timer can only be edited before the round starts.

) : ( - <> -

- Set how long the round will run. Applies when you start the round. -

- -
- -
- {value} - min -
- -
- -
- {PRESETS.map((p) => ( - - ))} -
- - {error &&

{error}

} - {saved && !dirty && ( -

Timer updated.

- )} - - - +
+ + change(val[0])} + className="pt-2 max-w-[377px]" + /> +
)}
+ + {isAdmin && !isRoundStarted && ( +
+ {error &&

{error}

} + {saved && !dirty && ( +

Timer updated.

+ )} + +
+ )}
); }; diff --git a/bsg-frontend/apps/extension/out/defaultPopup.html b/bsg-frontend/apps/extension/out/defaultPopup.html deleted file mode 100644 index 4b5df16c..00000000 --- a/bsg-frontend/apps/extension/out/defaultPopup.html +++ /dev/null @@ -1 +0,0 @@ -

BSG_

You are not on LeetCode. Once you go to the website you can open up the side panel to start solving!

\ No newline at end of file diff --git a/worker-service/.DS_Store b/worker-service/.DS_Store deleted file mode 100644 index 893dff2ec048267188830862bff234c0d2f950e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKL2uJA6n<{IHQj{N1Cw@)#B~~_Af|~+)^!K2%LE6QO0t9zEsdupT@O{IoZ&z0 zFL33T@LzUf-?LrRv>PXcP~|5*e=qj;?D$J!$3!H0P*TsoX0`_s)_+?>WVT;kp>aqC!J6AG)W8jrfzV-w>B;AOZ2+((tIp62bs z)7-$gg<7{CdXkGw3}bCg6fp0$v%MzsvD2^$SOxx`0=z!Ba7NEysZlK*DAW-E*g&;1 z#QeL!Ij%v^V5t!;Fria{I#rk>hS2G#cMZ=oSZdViB+TJMn307!p$IiP`gawbgs0J# zRspL(Re`#`Z1etq^7#3`nq;r60#<>wQb08Ne!qt)nZ0#oa=h2N@K10y=9L;%3JS9w gtAMxSUAQu|xf}pJgQZ5)!0eBJlED^Mfq$yNJro+FlmGw# From 6cbbc5d2d980542b0ed60de4764c5e3137c31b18 Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Sun, 28 Jun 2026 12:37:42 -0500 Subject: [PATCH 04/13] UI --- bsg-frontend/.next 2 | 1 + 1 file changed, 1 insertion(+) create mode 120000 bsg-frontend/.next 2 diff --git a/bsg-frontend/.next 2 b/bsg-frontend/.next 2 new file mode 120000 index 00000000..b8785493 --- /dev/null +++ b/bsg-frontend/.next 2 @@ -0,0 +1 @@ +.next.nosync \ No newline at end of file From e238c92ccc2290660000e040e4d45de4c2e35f8d Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Mon, 29 Jun 2026 15:05:37 -0500 Subject: [PATCH 05/13] timer timout msg added --- .../customComponents/RoomDisplays/SettingsDisplay.tsx | 11 +++++++++++ docker-compose.yml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx index fd51d280..a7a4d3df 100644 --- a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx +++ b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx @@ -63,6 +63,17 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { } }; + //timer update success message timeout + useEffect(() => { + if (!saved) return; + + const timeout = setTimeout(() => { + setSaved(false); + }, 3000); + + return () => clearTimeout(timeout); + }, [saved]); + return (
Settings
diff --git a/docker-compose.yml b/docker-compose.yml index c229ab44..dc564885 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -156,4 +156,4 @@ volumes: zookeeper-data-volume: zookeeper-log-volume: redisinsight-volume: - kafka-data-volume: + kafka-data-volume: \ No newline at end of file From 1626d141a0483a4a3e79ef931441961030e9f477 Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Wed, 1 Jul 2026 14:57:31 -0500 Subject: [PATCH 06/13] added chat sound -send --- .../apps/extension/hooks/useChatSocket.ts | 11 +++++++++++ .../extension/sounds/message-recieved.mp3 | Bin 0 -> 36864 bytes bsg-frontend/extension/sounds/message-sent.mp3 | Bin 0 -> 40704 bytes 3 files changed, 11 insertions(+) create mode 100644 bsg-frontend/extension/sounds/message-recieved.mp3 create mode 100644 bsg-frontend/extension/sounds/message-sent.mp3 diff --git a/bsg-frontend/apps/extension/hooks/useChatSocket.ts b/bsg-frontend/apps/extension/hooks/useChatSocket.ts index f5fc65d6..37a543f0 100644 --- a/bsg-frontend/apps/extension/hooks/useChatSocket.ts +++ b/bsg-frontend/apps/extension/hooks/useChatSocket.ts @@ -3,6 +3,16 @@ import { useRoomStore } from '@/stores/useRoomStore'; import { RTC_SERVICE_URL } from '../lib/config'; import { useUserStore } from '@/stores/useUserStore'; +//for audio unpload to out folder +function playChatSound(filename: string) { + if (typeof chrome === 'undefined' || !chrome.runtime?.getURL) return; + + const audio = new Audio(chrome.runtime.getURL(`sounds/${filename}`)); + audio.play().catch(() => { + // Chrome may block autoplay + }); +} + export type Message = { userHandle: string; userName?: string; @@ -198,6 +208,7 @@ export const useChatSocket = () => { }) }; socketRef.current.send(JSON.stringify(payload)); + playChatSound('message-sent.mp3'); setInputText(''); textArea.style.height = 'auto'; diff --git a/bsg-frontend/extension/sounds/message-recieved.mp3 b/bsg-frontend/extension/sounds/message-recieved.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0430b396b34a894384d8ecfc5394159c496ba6c7 GIT binary patch literal 36864 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 literal 0 HcmV?d00001 diff --git a/bsg-frontend/extension/sounds/message-sent.mp3 b/bsg-frontend/extension/sounds/message-sent.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4f61b6ec0e94117dfb615c745d433f2c76a3b12e GIT binary patch literal 40704 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 MUj6^`fA9tV1OGwfb^rhX literal 0 HcmV?d00001 From 45416aa4e621c1848b1aa7ee41792e018c2e9029 Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Wed, 1 Jul 2026 16:54:45 -0500 Subject: [PATCH 07/13] added sound send+ recieve --- .../apps/extension/hooks/useChatSocket.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bsg-frontend/apps/extension/hooks/useChatSocket.ts b/bsg-frontend/apps/extension/hooks/useChatSocket.ts index 37a543f0..6b331226 100644 --- a/bsg-frontend/apps/extension/hooks/useChatSocket.ts +++ b/bsg-frontend/apps/extension/hooks/useChatSocket.ts @@ -37,6 +37,7 @@ export const useChatSocket = () => { const atLimitTimeoutRef = useRef | null>(null); const categoryRefs = useRef>({}); const emojiMenuRef = useRef(null); + const suppressChatSoundsRef = useRef(false); //sound allowed const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(''); @@ -75,7 +76,9 @@ export const useChatSocket = () => { roomID: targetRoomID }) }; + suppressChatSoundsRef.current = true; // ws.send(JSON.stringify(payload)); + joinedRoomIDRef.current = targetRoomID; console.log("Sent join-room on socket open", { roomID: targetRoomID }); } @@ -98,9 +101,19 @@ export const useChatSocket = () => { roomID: message.roomID, isSystem: false }]); + + // checks user handle to know if the message is sent by the user or received from others + if (!suppressChatSoundsRef.current) { + if (message.userHandle === userEmail) { + playChatSound('message-sent.mp3'); + } else { + playChatSound('message-recieved.mp3'); + } + } } else if (responseType === 'system-announcement') { // Ignore connection-level join acks to avoid repeated chat noise on reconnects. - if (message?.data === 'Join Room Request') { + if (message?.data === 'Join Room Request') { // border between history and new msgs + suppressChatSoundsRef.current = false; return; } console.log('recieved system message: ' + message); @@ -165,6 +178,7 @@ export const useChatSocket = () => { // Clear messages when joining a new room so we don't see chat history from previous rooms setMessages([]); setLastGameEvent(null); + suppressChatSoundsRef.current = true; if (joinedRoomIDRef.current === roomID) { pendingRoomIDRef.current = roomID; @@ -182,6 +196,7 @@ export const useChatSocket = () => { roomID: roomID }) }; + suppressChatSoundsRef.current = true; socketRef.current.send(JSON.stringify(payload)); joinedRoomIDRef.current = roomID; } @@ -208,7 +223,6 @@ export const useChatSocket = () => { }) }; socketRef.current.send(JSON.stringify(payload)); - playChatSound('message-sent.mp3'); setInputText(''); textArea.style.height = 'auto'; From 7a1392565b2337dee78925f926a9e06b740d5745 Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Wed, 1 Jul 2026 20:51:27 -0500 Subject: [PATCH 08/13] added notification UI --- .../customComponents/NotificationToggle.tsx | 46 +++++++++++++++++++ .../RoomDisplays/SettingsDisplay.tsx | 17 +++++++ 2 files changed, 63 insertions(+) create mode 100644 bsg-frontend/apps/extension/customComponents/NotificationToggle.tsx diff --git a/bsg-frontend/apps/extension/customComponents/NotificationToggle.tsx b/bsg-frontend/apps/extension/customComponents/NotificationToggle.tsx new file mode 100644 index 00000000..6d00f8d6 --- /dev/null +++ b/bsg-frontend/apps/extension/customComponents/NotificationToggle.tsx @@ -0,0 +1,46 @@ +type NotificationToggleProps = { + enabled: boolean; + onChange: (enabled: boolean) => void; + disabled?: boolean; +}; + +export const NotificationToggle = ({ + enabled, + onChange, + disabled = false, +}: NotificationToggleProps) => ( + +); \ No newline at end of file diff --git a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx index a7a4d3df..d9d9ef94 100644 --- a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx +++ b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx @@ -3,6 +3,7 @@ import { useRoomStore } from '@/stores/useRoomStore'; import { SERVER_URL } from '../../lib/config'; import { Label } from '@bsg/ui/label'; import { Slider } from '@bsg/ui/slider'; +import { NotificationToggle } from '../NotificationToggle'; const MIN_DURATION = 5; const MAX_DURATION = 120; @@ -74,6 +75,8 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { return () => clearTimeout(timeout); }, [saved]); + const [chatNotificationsEnabled, setChatNotificationsEnabled] = useState(true); //temp + return (
Settings
@@ -106,6 +109,20 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => {
+ +
+
+ Chat sounds + + Play a sound when messages are sent or received + +
+ +
+ {isAdmin && !isRoundStarted && (
{error &&

{error}

} From ba04ade2ec2e1f151d6ed5c96b321f6604b9581e Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Wed, 1 Jul 2026 21:13:33 -0500 Subject: [PATCH 09/13] added notifcation backedn --- .../RoomDisplays/SettingsDisplay.tsx | 14 +++++++-- .../apps/extension/hooks/useChatSocket.ts | 7 +++-- .../apps/extension/pages/room-page.tsx | 6 ++++ .../apps/extension/stores/useSettingsStore.ts | 29 +++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 bsg-frontend/apps/extension/stores/useSettingsStore.ts diff --git a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx index d9d9ef94..c784b6cd 100644 --- a/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx +++ b/bsg-frontend/apps/extension/customComponents/RoomDisplays/SettingsDisplay.tsx @@ -4,11 +4,13 @@ import { SERVER_URL } from '../../lib/config'; import { Label } from '@bsg/ui/label'; import { Slider } from '@bsg/ui/slider'; import { NotificationToggle } from '../NotificationToggle'; +import { useSettingsStore } from '@/stores/useSettingsStore'; const MIN_DURATION = 5; const MAX_DURATION = 120; const STEP = 5; + export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { const roomId = useRoomStore(s => s.roomId); @@ -22,6 +24,16 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { const [ error, setError ] = useState(null); const [ saved, setSaved ] = useState(false); + + const chatNotificationsEnabled = useSettingsStore(s => s.chatNotificationsEnabled); + const setChatNotificationsEnabled = useSettingsStore(s => s.setChatNotificationsEnabled); + const loadSettings = useSettingsStore(s => s.loadSettings); + + useEffect(() => { + if (isActive) loadSettings(); + }, [isActive, loadSettings]); + + // Sync the working value when the stored duration changes or the tab reopens. useEffect(() => { if (roundDuration != null) setValue(roundDuration); @@ -75,8 +87,6 @@ export const SettingsDisplay = ({ isActive }: { isActive: boolean }) => { return () => clearTimeout(timeout); }, [saved]); - const [chatNotificationsEnabled, setChatNotificationsEnabled] = useState(true); //temp - return (
Settings
diff --git a/bsg-frontend/apps/extension/hooks/useChatSocket.ts b/bsg-frontend/apps/extension/hooks/useChatSocket.ts index 6b331226..0ace65b1 100644 --- a/bsg-frontend/apps/extension/hooks/useChatSocket.ts +++ b/bsg-frontend/apps/extension/hooks/useChatSocket.ts @@ -2,15 +2,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRoomStore } from '@/stores/useRoomStore'; import { RTC_SERVICE_URL } from '../lib/config'; import { useUserStore } from '@/stores/useUserStore'; +import { useSettingsStore } from '@/stores/useSettingsStore'; + //for audio unpload to out folder function playChatSound(filename: string) { if (typeof chrome === 'undefined' || !chrome.runtime?.getURL) return; + if (!useSettingsStore.getState().chatNotificationsEnabled) return; // checks if chat notifications are enabled const audio = new Audio(chrome.runtime.getURL(`sounds/${filename}`)); - audio.play().catch(() => { - // Chrome may block autoplay - }); + audio.play().catch(() => {}); } export type Message = { diff --git a/bsg-frontend/apps/extension/pages/room-page.tsx b/bsg-frontend/apps/extension/pages/room-page.tsx index 2098575c..185ea8a3 100644 --- a/bsg-frontend/apps/extension/pages/room-page.tsx +++ b/bsg-frontend/apps/extension/pages/room-page.tsx @@ -4,6 +4,8 @@ import { LeaderboardDisplay } from '@/customComponents/RoomDisplays/LeaderboardD import { StatisticsDisplay } from '@/customComponents/RoomDisplays/StatisticsDisplay'; import { SettingsDisplay } from '@/customComponents/RoomDisplays/SettingsDisplay'; import { useRoomStore } from '@/stores/useRoomStore'; +import { useSettingsStore } from '@/stores/useSettingsStore'; +import { useEffect } from 'react'; export default function RoomPage() { @@ -11,6 +13,10 @@ export default function RoomPage() { const roomNotice = useRoomStore(s => s.roomNotice) const setRoomNotice = useRoomStore(s => s.setRoomNotice) + useEffect(() => { + useSettingsStore.getState().loadSettings(); + }, []); // Load settings when the component mounts + return ( <> {roomNotice && ( diff --git a/bsg-frontend/apps/extension/stores/useSettingsStore.ts b/bsg-frontend/apps/extension/stores/useSettingsStore.ts new file mode 100644 index 00000000..821332e0 --- /dev/null +++ b/bsg-frontend/apps/extension/stores/useSettingsStore.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; + +interface SettingsStoreState { + chatNotificationsEnabled: boolean; + setChatNotificationsEnabled: (enabled: boolean) => void; + loadSettings: () => void; +} + +export const useSettingsStore = create((set) => ({ + chatNotificationsEnabled: true, + + setChatNotificationsEnabled: (enabled) => { + set({ chatNotificationsEnabled: enabled }); + + if (typeof chrome !== 'undefined' && chrome.storage?.local) { + chrome.storage.local.set({ chatNotificationsEnabled: enabled }); + } + }, + + loadSettings: () => { + if (typeof chrome !== 'undefined' && chrome.storage?.local) { + chrome.storage.local.get(['chatNotificationsEnabled'], (result) => { + if (typeof result.chatNotificationsEnabled === 'boolean') { + set({ chatNotificationsEnabled: result.chatNotificationsEnabled }); + } + }); + } + }, +})); \ No newline at end of file From eb06664b4504ab1391963779a7aed2c4c3f2e12f Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Fri, 10 Jul 2026 14:34:29 -0500 Subject: [PATCH 10/13] notification UI added --- .../customComponents/TabBar/Sidebar.tsx | 37 ++++++++++++++++++- .../apps/extension/hooks/useChatSocket.ts | 27 ++++++++++++++ bsg-frontend/apps/extension/pages/_app.tsx | 16 ++++++++ .../apps/extension/stores/usePanelStore.ts | 10 +++-- .../apps/extension/stores/useRoomStore.ts | 18 ++++++++- 5 files changed, 103 insertions(+), 5 deletions(-) diff --git a/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx b/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx index b1f8d0e9..9b6d31eb 100644 --- a/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx +++ b/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx @@ -15,6 +15,8 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => { const activeTab = useRoomStore(s => s.activeTab); const setActiveTab = useRoomStore(s => s.setActiveTab); const isPanelHovered = usePanelStore(s => s.isPanelHovered); + + const unreadCount = useRoomStore(s => s.unreadCount); return (
{ {/* Logo */}
-
+
{ stroke-linejoin="round" /> + + {unreadCount > 0 && ( + + {unreadCount > 9 ? ( + <> + 9 + + + + ) : ( + unreadCount + )} + + )}
diff --git a/bsg-frontend/apps/extension/hooks/useChatSocket.ts b/bsg-frontend/apps/extension/hooks/useChatSocket.ts index 0ace65b1..ca26d05c 100644 --- a/bsg-frontend/apps/extension/hooks/useChatSocket.ts +++ b/bsg-frontend/apps/extension/hooks/useChatSocket.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRoomStore } from '@/stores/useRoomStore'; +import { usePanelStore } from '@/stores/usePanelStore'; import { RTC_SERVICE_URL } from '../lib/config'; import { useUserStore } from '@/stores/useUserStore'; import { useSettingsStore } from '@/stores/useSettingsStore'; @@ -14,6 +15,13 @@ function playChatSound(filename: string) { audio.play().catch(() => {}); } +// for chat notification count - increment +function isChatVisible(): boolean { + const { activeTab } = useRoomStore.getState(); + const { isFolded } = usePanelStore.getState(); + return activeTab === 'chat' && !isFolded; + } + export type Message = { userHandle: string; userName?: string; @@ -111,6 +119,24 @@ export const useChatSocket = () => { playChatSound('message-recieved.mp3'); } } + + // for chat notification count - increment + const isNewMessage = !suppressChatSoundsRef.current; + const fromOtherUser = message.userHandle !== userEmail; + const chatVisible = isChatVisible(); + console.log('[BSG unread] message check', { + isNewMessage, + fromOtherUser, + chatVisible, + userHandle: message.userHandle, + userEmail, + activeTab: useRoomStore.getState().activeTab, + isFolded: usePanelStore.getState().isFolded, + currentUnread: useRoomStore.getState().unreadCount, + }); + if (isNewMessage && fromOtherUser && !chatVisible) { + useRoomStore.getState().incrementUnread(); + } } else if (responseType === 'system-announcement') { // Ignore connection-level join acks to avoid repeated chat noise on reconnects. if (message?.data === 'Join Room Request') { // border between history and new msgs @@ -180,6 +206,7 @@ export const useChatSocket = () => { setMessages([]); setLastGameEvent(null); suppressChatSoundsRef.current = true; + useRoomStore.getState().clearUnread(); // checks uread if (joinedRoomIDRef.current === roomID) { pendingRoomIDRef.current = roomID; diff --git a/bsg-frontend/apps/extension/pages/_app.tsx b/bsg-frontend/apps/extension/pages/_app.tsx index b35850f9..2131d4c7 100644 --- a/bsg-frontend/apps/extension/pages/_app.tsx +++ b/bsg-frontend/apps/extension/pages/_app.tsx @@ -11,6 +11,7 @@ import { Footer } from '@/customComponents/Footer/Footer'; import { useRoomStore } from '@/stores/useRoomStore'; import { messageScript } from '@/utils/messageScript'; import { useIsActive } from '@/hooks/useIsActive'; +import { useEffect } from 'react'; const poppins = Poppins({ weight: '400', subsets: ['latin'] }); @@ -21,6 +22,21 @@ export default function App({ Component, pageProps }: AppProps) { const setIsPanelHovered = usePanelStore(s => s.setIsPanelHovered); const isInRoom = useRoomStore(s => s.isInRoom); const { isActive, setIsActive } = useIsActive(); + const activeTab = useRoomStore(s => s.activeTab); + const clearUnread = useRoomStore(s => s.clearUnread); + + const setIsFolded = usePanelStore(s => s.setIsFolded); + + useEffect(() => { + setIsFolded(isFolded); + }, [isFolded, setIsFolded]); + + useEffect(() => { + if (activeTab === 'chat' && !isFolded) { + console.log('[BSG unread] clearing — chat is active'); + clearUnread(); + } + }, [activeTab, isFolded, clearUnread]); // Redirect popup render if (isDefaultPopup) { diff --git a/bsg-frontend/apps/extension/stores/usePanelStore.ts b/bsg-frontend/apps/extension/stores/usePanelStore.ts index 62b3a2ef..f291cb9c 100644 --- a/bsg-frontend/apps/extension/stores/usePanelStore.ts +++ b/bsg-frontend/apps/extension/stores/usePanelStore.ts @@ -3,9 +3,13 @@ import { create } from 'zustand'; interface panelStoreState { isPanelHovered: boolean; setIsPanelHovered: (isPanelHovered: boolean) => void; + isFolded: boolean; + setIsFolded: (isFolded: boolean) => void; } export const usePanelStore = create((set) => ({ - isPanelHovered: false, - setIsPanelHovered: (isPanelHovered) => set({ isPanelHovered: isPanelHovered }), -})); \ No newline at end of file + isPanelHovered: false, + setIsPanelHovered: (isPanelHovered) => set({ isPanelHovered }), + isFolded: false, + setIsFolded: (isFolded) => set({ isFolded }), +})); diff --git a/bsg-frontend/apps/extension/stores/useRoomStore.ts b/bsg-frontend/apps/extension/stores/useRoomStore.ts index ba9ae950..7805be87 100644 --- a/bsg-frontend/apps/extension/stores/useRoomStore.ts +++ b/bsg-frontend/apps/extension/stores/useRoomStore.ts @@ -17,7 +17,10 @@ interface roomStoreState { roundDuration: number | null; lastGameEvent: GameEvent | null; roomNotice: string | null; + unreadCount: number; // for chat notification count + incrementUnread: () => void; // for chat notification count + clearUnread: () => void; // for chat notification count setIsInRoom: (isInRoom: boolean) => void; setRoomId: (roomId: string | null) => void; setIsConnected: (isConnected: boolean) => void; @@ -55,6 +58,7 @@ const roomStoreInit = { roundDuration: null, lastGameEvent: null, roomNotice: null, + unreadCount: 0, // set to 0 initially }; export const useRoomStore = create((set) => ({ @@ -72,7 +76,18 @@ export const useRoomStore = create((set) => ({ setRoundEndTime: (roundEndTime) => set({ roundEndTime: roundEndTime }), setRoundDuration: (roundDuration) => set({ roundDuration: roundDuration }), setLastGameEvent: (lastGameEvent) => set({ lastGameEvent: lastGameEvent }), + // for chat notification count setRoomNotice: (roomNotice) => set({ roomNotice: roomNotice }), + incrementUnread: () => set((state) => { + const unreadCount = state.unreadCount + 1; + console.log('[BSG unread] increment →', unreadCount); + return { unreadCount }; + }), + clearUnread: () => { + console.log('[BSG unread] clear → 0'); + set({ unreadCount: 0 }); + }, + initRoom: ( roomId, @@ -84,7 +99,8 @@ export const useRoomStore = create((set) => ({ roomCode: roomCode, adminId: adminId, isAdmin: isAdmin, - isInRoom: true + isInRoom: true, + unreadCount: 0 }), resetRoom: () => set(roomStoreInit) })); \ No newline at end of file From 8e4fee0b3b2434da14190f1a97ae3013b0db6a8a Mon Sep 17 00:00:00 2001 From: Sneha Lal Date: Sat, 11 Jul 2026 11:11:20 -0500 Subject: [PATCH 11/13] added tab navigation --- .../extension/customComponents/TabBar/Sidebar.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx b/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx index 9b6d31eb..9f6fd559 100644 --- a/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx +++ b/bsg-frontend/apps/extension/customComponents/TabBar/Sidebar.tsx @@ -18,6 +18,11 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => { const unreadCount = useRoomStore(s => s.unreadCount); + //to select tab and unfold the panel - sets active tab and unfolds the extension! + const selectTabAndUnfold = (tab: TabName) => { + setActiveTab(tab); + messageScript('UNFOLD'); + }; return (
messageScript('UNFOLD')} @@ -100,7 +105,7 @@ export const Sidebar = ({ isInRoom }: { isInRoom: boolean }) => {