From dc1e5c17480bd132f7f0cafeca38e4de2c1a70e1 Mon Sep 17 00:00:00 2001 From: slawk0 Date: Tue, 7 Jan 2025 18:30:09 +0100 Subject: [PATCH] refactored modals, added unread messages when user is not on page, --- client/package-lock.json | 382 ++++++++++++++---- client/package.json | 2 +- .../components/chat/chatArea/MessagesArea.tsx | 141 ++++--- .../chat/chatHeader/AddGroupMember.tsx | 168 ++++---- .../chat/chatHeader/ContactProfile.tsx | 5 +- .../chat/chatHeader/CreateGroupButton.tsx | 114 +++--- .../chat/leftSidebar/ContactsList.tsx | 14 +- .../chat/rightSidebar/ParticipantsBar.tsx | 9 +- client/src/components/ui/button.tsx | 2 +- client/src/components/ui/dialog.tsx | 120 ++++++ client/src/context/chat/ChatProvider.tsx | 11 +- client/src/hooks/useWindowFocus.tsx | 20 + client/src/types/types.ts | 10 +- server/db/db.js | 9 +- server/server.js | 26 +- 15 files changed, 715 insertions(+), 318 deletions(-) create mode 100644 client/src/components/ui/dialog.tsx create mode 100644 client/src/hooks/useWindowFocus.tsx diff --git a/client/package-lock.json b/client/package-lock.json index cc061fe..eca56dc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-context-menu": "^2.2.3", - "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", @@ -1152,6 +1152,67 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", @@ -1378,25 +1439,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", - "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.2", - "@radix-ui/react-presence": "1.1.1", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.6.0" + "react-remove-scroll": "^2.6.1" }, "peerDependencies": { "@types/react": "*", @@ -1413,6 +1474,168 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", + "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -1471,13 +1694,13 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { @@ -1495,6 +1718,62 @@ } } }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -1624,31 +1903,6 @@ } } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", - "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", @@ -4081,15 +4335,6 @@ "node": ">=0.8.19" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4988,20 +5233,20 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -5042,21 +5287,20 @@ } }, "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", - "invariant": "^2.2.4", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5658,9 +5902,9 @@ } }, "node_modules/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -5669,8 +5913,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { diff --git a/client/package.json b/client/package.json index 6fb178c..7b6ce37 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,7 @@ "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-context-menu": "^2.2.3", - "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", diff --git a/client/src/components/chat/chatArea/MessagesArea.tsx b/client/src/components/chat/chatArea/MessagesArea.tsx index 30cc03d..f83dd5b 100644 --- a/client/src/components/chat/chatArea/MessagesArea.tsx +++ b/client/src/components/chat/chatArea/MessagesArea.tsx @@ -1,11 +1,12 @@ import { useContext, useEffect, useRef, useState } from 'react'; import { socket } from '@/socket/socket.ts'; -import { sendContact } from '@/api/contactsApi.tsx'; +import { sendContact, setContactStatus } from '@/api/contactsApi.tsx'; import LoadingWheel from '../LoadingWheel.tsx'; import AnimatedMessage from '@/components/chat/chatArea/AnimatedMessage.tsx'; import { ChatMessagesProps } from '@/types/types.ts'; import { useChat } from '@/context/chat/useChat.ts'; import { AuthContext } from '@/utils/AuthProvider.tsx'; +import { useWindowFocus } from '@/hooks/useWindowFocus.tsx'; function MessagesArea() { const { @@ -17,12 +18,15 @@ function MessagesArea() { setContactsList, errorMessage, fetchPreviousMessages, + setCurrentContact, } = useChat(); const containerRef = useRef(null); const { user } = useContext(AuthContext); const [isLoading, setIsLoading] = useState(false); const [shouldScrollToBottom, setShouldScrollToBottom] = useState(true); const previousMessagesLength = useRef(messages.length); + const isFocused = useWindowFocus(); + const previousTitle = useRef(document.title); const scrollToBottom = () => { const container = containerRef.current; @@ -80,7 +84,6 @@ function MessagesArea() { useEffect(() => { if (!socket) return; - const currentContainer = containerRef.current; if (currentContainer) { currentContainer.addEventListener('scroll', handleScroll); @@ -88,12 +91,27 @@ function MessagesArea() { socket.on('chat message', (msg: ChatMessagesProps) => { console.log('Received message: ', msg); - if ( - msg.conversation_id !== currentContact?.conversation_id && - msg.sender !== user?.username - ) { - setContactsList((prevContacts) => { - // Find if contact already exists1 + + const isFromCurrentConversation = + msg.conversation_id === currentContact?.conversation_id; + const isFromCurrentUser = msg.sender === user?.username; + + if (isFromCurrentConversation) { + messageHandler(msg); + if (!isFocused) { + updateContactStatus(msg.conversation_id, false); + setCurrentContact({ ...currentContact, read: false }); + document.title = 'New message ❗'; + } + } + + // Handle contact list updates + setContactsList((prevContacts) => { + // If message is from another conversation + if (!isFromCurrentConversation && !isFromCurrentUser) { + setContactStatus(msg.conversation_id, false); + + // Check if contact exists const existingContact = prevContacts.find( (c) => c.conversation_id === msg.conversation_id, ); @@ -116,65 +134,23 @@ function MessagesArea() { last_message_time: new Date().toString(), }, ]; - } else { - return prevContacts.map((contact) => - contact.conversation_id === msg.conversation_id - ? { - ...contact, - last_active: new Date().toString(), - last_message: msg.message, - last_message_sender: msg.sender, - last_message_time: new Date().toString(), - } - : contact, - ); } - }); - - // Update contact status... - } else { - if (msg.conversation_id == currentContact?.conversation_id) { - messageHandler(msg); - - setContactsList((prevContacts) => { - const existingContact = prevContacts.find( - (c) => c.conversation_id === msg.conversation_id, - ); - - if (!existingContact) { - sendContact(msg.sender); - return [ - ...prevContacts, - { - username: msg.sender, - read: false, - id: msg.message_id, - user_id: msg.sender_id, - conversation_id: msg.conversation_id, - type: 'direct', - last_active: new Date().toString(), - last_message: msg.message, - last_message_id: msg.message_id, - last_message_sender: msg.sender, - last_message_time: new Date().toString(), - }, - ]; - } else { - return prevContacts.map((contact) => - contact.conversation_id === msg.conversation_id - ? { - ...contact, - last_active: new Date().toString(), - last_message: msg.message, - last_message_sender: msg.sender, - last_message_time: new Date().toString(), - } - : contact, - ); - } - }); } - } + + // Update existing contact if found + return prevContacts.map((contact) => + contact.conversation_id === msg.conversation_id + ? { + ...contact, + read: isFromCurrentConversation ? isFocused : false, + last_active: new Date().toString(), + last_message: msg.message, + last_message_sender: msg.sender, + last_message_time: new Date().toString(), + } + : contact, + ); + }); }); socket.on( @@ -213,7 +189,40 @@ function MessagesArea() { socket.off('chat message'); socket.off('delete message'); }; - }, [currentContact, user?.username, setContactsList, updateContactStatus]); + }, [ + currentContact, + user?.username, + setContactsList, + updateContactStatus, + isFocused, + ]); + + useEffect(() => { + if (isFocused && currentContact?.read == true) { + return; + } + document.title = previousTitle.current; + + if (currentContact?.conversation_id && currentContact?.read !== true) { + updateContactStatus(currentContact.conversation_id, true); + setCurrentContact({ ...currentContact, read: true }); + } + + const timeout = setTimeout(() => { + setContactsList((prevContacts) => { + return prevContacts.map((contact) => + contact.conversation_id === currentContact?.conversation_id + ? { + ...contact, + read: true, + } + : contact, + ); + }); + }, 1000); + + return () => clearTimeout(timeout); + }, [isFocused]); useEffect(() => { const hasNewMessages = messages.length > previousMessagesLength.current; diff --git a/client/src/components/chat/chatHeader/AddGroupMember.tsx b/client/src/components/chat/chatHeader/AddGroupMember.tsx index 2f1c873..5d7dd7b 100644 --- a/client/src/components/chat/chatHeader/AddGroupMember.tsx +++ b/client/src/components/chat/chatHeader/AddGroupMember.tsx @@ -1,12 +1,20 @@ -import LoadingWheel from '../LoadingWheel.tsx'; -import { useEffect, useRef, useState } from 'react'; +import LoadingWheel from '../LoadingWheel'; +import { useEffect, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { socket } from '@/socket/socket.ts'; +import { socket } from '@/socket/socket'; import { UserRoundPlus } from 'lucide-react'; -import { Button } from '@/components/ui/button.tsx'; -import { useChat } from '@/context/chat/useChat.ts'; -import { axiosClient } from '@/utils/axiosClient.ts'; +import { Button } from '@/components/ui/button'; +import { useChat } from '@/context/chat/useChat'; +import { axiosClient } from '@/utils/axiosClient'; import axios from 'axios'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; type Inputs = { username: string; @@ -14,8 +22,8 @@ type Inputs = { function AddGroupMember() { const { currentContact } = useChat(); + const [open, setOpen] = useState(false); const { register, handleSubmit, watch, reset } = useForm(); - const modalRef = useRef(null); const contactInput = watch('username'); const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -66,7 +74,7 @@ function AddGroupMember() { return () => clearTimeout(delay); }, [contactInput]); - if (!socket) return; + if (!socket) return null; const onSubmit: SubmitHandler = async (data) => { const contactToSubmit = @@ -86,9 +94,7 @@ function AddGroupMember() { socket?.emit('added to group', { group_id: currentContact?.conversation_id, }); - if (modalRef.current) { - modalRef.current.close(); - } + setOpen(false); reset(); } catch (e) { if (axios.isAxiosError(e)) { @@ -126,92 +132,82 @@ function AddGroupMember() { }; return ( - <> -
- - -
-
-
- -
-
- -
-

Enter username

-
-
- -
-
- {suggestions?.length > 0 ? ( -
    - {suggestions.map((suggestion, index) => ( -
  • { - handleSubmit(() => - onSubmit({ username: suggestion }), - )(); - setSuggestions([]); - }} - > - {suggestion} -
  • - ))} -
- ) : isLoading ? ( + + + + + Enter username + + +
+
+
+ +
+ {suggestions?.length > 0 ? ( +
    + {suggestions.map((suggestion, index) => ( +
  • { + handleSubmit(() => + onSubmit({ username: suggestion }), + )(); + setSuggestions([]); + }} + > + {suggestion} +
  • + ))} +
+ ) : isLoading ? ( +
- ) : null} - {notFound ? ( -

- user not found -

- ) : null} -
+
+ ) : null} + {notFound && ( +

+ user not found +

+ )}
- {errorMessage && ( -

- {errorMessage} -

- )}
- -
+ {errorMessage && ( +

+ {errorMessage} +

+ )} +
-
-
-
- + + + + ); } diff --git a/client/src/components/chat/chatHeader/ContactProfile.tsx b/client/src/components/chat/chatHeader/ContactProfile.tsx index 4952a9b..21b15cb 100644 --- a/client/src/components/chat/chatHeader/ContactProfile.tsx +++ b/client/src/components/chat/chatHeader/ContactProfile.tsx @@ -15,10 +15,7 @@ function ContactProfile() { profile img )} -

- {currentContact ? currentContact.username : null} user id:{' '} - {currentContact?.user_id} conv id: {currentContact?.conversation_id} -

+

{currentContact ? currentContact.username : null}

diff --git a/client/src/components/chat/chatHeader/CreateGroupButton.tsx b/client/src/components/chat/chatHeader/CreateGroupButton.tsx index 6fe2298..1e906a8 100644 --- a/client/src/components/chat/chatHeader/CreateGroupButton.tsx +++ b/client/src/components/chat/chatHeader/CreateGroupButton.tsx @@ -1,8 +1,17 @@ -import LoadingWheel from '../LoadingWheel.tsx'; -import { useRef, useState } from 'react'; +import LoadingWheel from '../LoadingWheel'; +import { useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; import { Plus } from 'lucide-react'; -import { axiosClient } from '@/utils/axiosClient.ts'; +import { axiosClient } from '@/utils/axiosClient'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; type Inputs = { groupName: string; @@ -10,7 +19,7 @@ type Inputs = { function CreateGroupButton() { const [isLoading, setIsLoading] = useState(false); - const modalRef = useRef(null); + const [open, setOpen] = useState(false); const { register, @@ -26,9 +35,7 @@ function CreateGroupButton() { console.log(response.data); if (response.data.group_id) { setIsLoading(false); - if (modalRef.current) { - modalRef.current.close(); - } + setOpen(false); } reset(); } catch (e) { @@ -38,63 +45,48 @@ function CreateGroupButton() { }; return ( - <> -
- - -
-
-
- -
-
- -
-

Enter group name

-
- - {errors.groupName?.type === 'minLength' && ( -

group name is too short

- )} - {errors.groupName?.type === 'maxLength' && ( -

group name is too long

- )} -
-
- -
- -
+ + + + + Enter group name + + +
+
+ + {errors.groupName?.type === 'minLength' && ( +

Group name is too short

+ )} + {errors.groupName?.type === 'maxLength' && ( +

Group name is too long

+ )}
-
-
- +
+ +
+ + + ); } diff --git a/client/src/components/chat/leftSidebar/ContactsList.tsx b/client/src/components/chat/leftSidebar/ContactsList.tsx index b053db2..a5876b4 100644 --- a/client/src/components/chat/leftSidebar/ContactsList.tsx +++ b/client/src/components/chat/leftSidebar/ContactsList.tsx @@ -99,7 +99,7 @@ function ContactsList() { className="m-1 flex p-2 hover:bg-zinc-900 cursor-pointer transition-colors rounded-lg justify-between items-start min-h-[40px]" onClick={() => { initializeContact(contact); - updateContactStatus(contact, true); + updateContactStatus(contact.conversation_id, true); }} >
@@ -114,7 +114,7 @@ function ContactsList() { /> )}

- {contact.read ? '•' : '•'} + {contact.read ? '' : '•'}

@@ -133,7 +133,7 @@ function ContactsList() { Leave Group? - + Are you sure you want to leave this group? @@ -164,12 +164,12 @@ function ContactsList() { )} -
+
- {contact.last_message?.length > 0 ? ( - contact.last_message?.length > 16 ? ( + {(contact.last_message ?? '').length > 0 ? ( + (contact.last_message ?? '').length > 15 ? (
- {contact.last_message?.substring(0, 16)} + {contact.last_message?.substring(0, 15)}
) : ( diff --git a/client/src/components/chat/rightSidebar/ParticipantsBar.tsx b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx index 82c9332..a0b86a8 100644 --- a/client/src/components/chat/rightSidebar/ParticipantsBar.tsx +++ b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx @@ -135,6 +135,10 @@ function ParticipantsBar() { group_id: string; isadmin: boolean; isowner: boolean; + last_message_id: number; + last_message: string | null; + last_message_time: string | null; + last_message_sender: string | null; }) => { const { group_id } = msg; if ( @@ -150,8 +154,9 @@ function ParticipantsBar() { conversation_id: msg.group_id, last_active: new Date().toString(), last_message: '', - last_message_sender: '', - last_message_time: '', + last_message_id: msg.last_message_id, + last_message_sender: msg.last_message_sender, + last_message_time: msg.last_message_time, }); } diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index 92bde7b..1824e66 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -18,7 +18,7 @@ const buttonVariants = cva( secondary: 'bg-gray-100 text-gray-900 shadow-sm hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80', ghost: - 'hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50', + 'hover:bg-zinc-800 dark:hover:bg-gray-800 dark:hover:text-gray-50', link: 'text-gray-900 underline-offset-4 hover:underline dark:text-gray-50', }, size: { diff --git a/client/src/components/ui/dialog.tsx b/client/src/components/ui/dialog.tsx new file mode 100644 index 0000000..a0b6caf --- /dev/null +++ b/client/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/client/src/context/chat/ChatProvider.tsx b/client/src/context/chat/ChatProvider.tsx index 6f1b79f..60150b6 100644 --- a/client/src/context/chat/ChatProvider.tsx +++ b/client/src/context/chat/ChatProvider.tsx @@ -88,15 +88,14 @@ export const ChatProvider = ({ children }: { children: ReactNode }) => { }); } - function updateContactStatus(contactObj: ContactsProps, read: boolean) { - console.log('Update contact status: ', contactObj); + function updateContactStatus(conversation_id: string, read: boolean) { + console.log('Update contact status: ', conversation_id); setContactsList((prevContacts) => prevContacts.map((contact) => { - if (contact.conversation_id === contactObj.conversation_id) { - if (!contactObj.read) { - setContactStatus(contactObj.conversation_id, read); - } + if (contact.conversation_id === conversation_id) { + setContactStatus(conversation_id, read); + return { ...contact, read: read }; } else { return contact; diff --git a/client/src/hooks/useWindowFocus.tsx b/client/src/hooks/useWindowFocus.tsx new file mode 100644 index 0000000..acfe4ba --- /dev/null +++ b/client/src/hooks/useWindowFocus.tsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +export function useWindowFocus() { + const [isFocused, setIsFocused] = useState(document.hasFocus()); + + useEffect(() => { + const onFocus = () => setIsFocused(true); + const onBlur = () => setIsFocused(false); + + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onBlur); + + return () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + }; + }, []); + + return isFocused; +} diff --git a/client/src/types/types.ts b/client/src/types/types.ts index 614e726..7cfbfe5 100644 --- a/client/src/types/types.ts +++ b/client/src/types/types.ts @@ -22,10 +22,10 @@ export type ContactsProps = { type: 'direct' | 'group'; conversation_id: string; last_active: string; - last_message: string; - last_message_id: number; - last_message_time: string; - last_message_sender: string; + last_message: string | null; + last_message_id: number | null; + last_message_time: string | null; + last_message_sender: string | null; }; export type ParticipantsProps = { @@ -67,6 +67,6 @@ export type ChatContextType = { initializeContact: (newContact: ContactsProps) => Promise; fetchMessages: (conversation_id: string) => Promise; messageHandler: (msg: ChatMessagesProps) => void; - updateContactStatus: (contactObj: ContactsProps, read: boolean) => void; + updateContactStatus: (conversation_id: string, read: boolean) => void; fetchPreviousMessages: (contact: string | null) => Promise; }; diff --git a/server/db/db.js b/server/db/db.js index 299b521..c2bed08 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -643,7 +643,7 @@ async function insertContact(initiatorId, receiverId, contactUsername, read) { conversation_id: contact.conversation_id, type: "direct", read: contact.read, - last_message_id: latestMessage.message_id, + last_message_id: latestMessage.last_message_id, last_message: latestMessage.last_message, last_message_time: latestMessage.last_message_time, last_message_sender: latestMessage.last_message_sender, @@ -657,7 +657,7 @@ async function insertContact(initiatorId, receiverId, contactUsername, read) { async function getLatestMessage(conversation_id) { const query = ` SELECT DISTINCT ON (m.conversation_id) - m.message_id, + m.message_id AS last_message_id, m.content AS last_message, m.sent_at AS last_message_time, a.username AS last_message_sender @@ -672,7 +672,7 @@ async function getLatestMessage(conversation_id) { const result = await client.query(query, [conversation_id]); return { - message_id: result.rows[0]?.message_id || null, + last_message_id: result.rows[0]?.last_message_id || null, last_message: result.rows[0]?.last_message || null, last_message_time: result.rows[0]?.last_message_time || null, last_message_sender: result.rows[0]?.last_message_sender || null, @@ -872,7 +872,7 @@ async function updateContactStatus(user_id, conversation_id, read) { `; try { await client.query(query, [read, user_id, conversation_id]); - await updateContactLastActive(user_id, conversation_id); + // await updateContactLastActive(user_id, conversation_id); console.log( `Successfully updated contact status, user_id: ${user_id}, conversation_id: ${conversation_id}, read: ${read}: `, ); @@ -1261,4 +1261,5 @@ module.exports = { isAdmin, addAdministrator, removeAdministrator, + getLatestMessage, }; diff --git a/server/server.js b/server/server.js index f52079d..3be4895 100644 --- a/server/server.js +++ b/server/server.js @@ -44,6 +44,7 @@ const { insertContactByUsername, isConversationMember, isAdmin, + getLatestMessage, } = require("./db/db"); const { extname } = require("node:path"); @@ -376,7 +377,10 @@ app.post("/api/chat/groups/create", authorizeUser, async (req, res) => { } console.log("Successfully created group: ", groupname, "id: ", group_id); console.log(`io.to: ${contact_user_id} added to group`); - io.to(contact_user_id).emit("added to group", { group_id }); + io.to(contact_user_id).emit("added to group", { + group_id, + username: groupname, + }); return res.status(200).json({ message: `Successfully created group: ${groupname}`, group_id: group_id, @@ -400,12 +404,22 @@ app.post("/api/chat/groups/addMember", authorizeUser, async (req, res) => { } const result = await addMemberToGroupByUsername(group_id, username); + const lastMessage = await getLatestMessage(group_id); + console.error("ADDED TO GROUP: ", { + username, + user_id: result, + group_id, + ...lastMessage, + }); if (result !== null) { - io.to(result).to(group_id).emit("added to group", { - username, - user_id: result, - group_id, - }); + io.to(result) + .to(group_id) + .emit("added to group", { + username, + user_id: result, + group_id, + ...lastMessage, + }); console.log("added to group: ", result); return res.status(200).json({ message: "Successfully added member" }); }