diff --git a/client/package-lock.json b/client/package-lock.json index 93b492582..141c819b6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -85,6 +85,7 @@ "web-vitals": "^3.5.2" }, "devDependencies": { + "@ant-design/icons": "^5.5.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.24.7", "@dotenvx/dotenvx": "^1.14.1", diff --git a/client/package.json b/client/package.json index 38d148706..fdb2b6b34 100644 --- a/client/package.json +++ b/client/package.json @@ -132,6 +132,7 @@ "@rollup/rollup-linux-x64-gnu": "4.6.1" }, "devDependencies": { + "@ant-design/icons": "^5.5.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.24.7", "@dotenvx/dotenvx": "^1.14.1", diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx index 076941d8c..81d3296fc 100644 --- a/client/src/components/chat-messages-list/chat-message-list.component.jsx +++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx @@ -1,11 +1,6 @@ -import Icon from "@ant-design/icons"; -import { Tooltip } from "antd"; -import i18n from "i18next"; -import dayjs from "../../utils/day"; -import React, { useRef, useEffect } from "react"; -import { MdDone, MdDoneAll } from "react-icons/md"; +import React, { useEffect, useRef } from "react"; import { Virtuoso } from "react-virtuoso"; -import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { renderMessage } from "./renderMessage"; import "./chat-message-list.styles.scss"; export default function ChatMessageListComponent({ messages }) { @@ -21,7 +16,7 @@ export default function ChatMessageListComponent({ messages }) { }); } }, 100); // Delay of 100ms to allow rendering - return () => clearTimeout(timer); // Cleanup the timer on unmount + return () => clearTimeout(timer); }, [messages.length]); // Run only once on component mount // Scroll to the bottom after the new messages are rendered @@ -37,52 +32,13 @@ export default function ChatMessageListComponent({ messages }) { }, 50); // Slight delay to ensure layout recalculates } }, [messages]); // Triggered when new messages are added - //TODO: Does this one need to come into the render of the method? - const renderMessage = (index) => { - const message = messages[index]; - return ( -
-
- -
- {message.image_path && - message.image_path.map((i, idx) => ( -
- - Received - -
- ))} -
{message.text}
-
-
- {message.status && ( -
- -
- )} -
- {message.isoutbound && ( -
- {i18n.t("messaging.labels.sentby", { - by: message.userid, - time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a") - })} -
- )} -
- ); - }; return (
renderMessage(index)} + itemContent={(index) => renderMessage(messages, index)} // Pass `messages` to renderMessage followOutput="smooth" // Ensure smooth scrolling when new data is appended style={{ height: "100%", width: "100%" }} /> diff --git a/client/src/components/chat-messages-list/renderMessage.jsx b/client/src/components/chat-messages-list/renderMessage.jsx new file mode 100644 index 000000000..e6982f72b --- /dev/null +++ b/client/src/components/chat-messages-list/renderMessage.jsx @@ -0,0 +1,42 @@ +import Icon from "@ant-design/icons"; +import { Tooltip } from "antd"; +import i18n from "i18next"; +import dayjs from "../../utils/day"; +import { MdDone, MdDoneAll } from "react-icons/md"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; + +export const renderMessage = (messages, index) => { + const message = messages[index]; + return ( +
+
+ +
+ {message.image_path && + message.image_path.map((i, idx) => ( +
+ + Received + +
+ ))} +
{message.text}
+
+
+ {message.status && (message.status === "sent" || message.status === "delivered") && ( +
+ +
+ )} +
+ {message.isoutbound && ( +
+ {i18n.t("messaging.labels.sentby", { + by: message.userid, + time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a") + })} +
+ )} +
+ ); +}; diff --git a/client/src/components/wss-status-display/wss-status-display.component.jsx b/client/src/components/wss-status-display/wss-status-display.component.jsx index 40ff0e42a..6eb2c9223 100644 --- a/client/src/components/wss-status-display/wss-status-display.component.jsx +++ b/client/src/components/wss-status-display/wss-status-display.component.jsx @@ -1,18 +1,33 @@ import { connect } from "react-redux"; -import { GlobalOutlined } from "@ant-design/icons"; +import { GlobalOutlined, WarningOutlined } from "@ant-design/icons"; import { createStructuredSelector } from "reselect"; import React from "react"; import { selectWssStatus } from "../../redux/application/application.selectors"; + const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser wssStatus: selectWssStatus }); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); -export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay); + +const mapDispatchToProps = (dispatch) => ({}); export function WssStatusDisplay({ wssStatus }) { console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus); - return ; + + let icon; + let color; + + if (wssStatus === "connected") { + icon = ; + color = "green"; + } else if (wssStatus === "error") { + icon = ; + color = "red"; + } else { + icon = ; + color = "gray"; // Default for other statuses like "disconnected" + } + + return {icon}; } + +export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay); diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 885602fe3..c13141ca7 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -3,75 +3,102 @@ import SocketIO from "socket.io-client"; import { auth } from "../../firebase/firebase.utils"; import { store } from "../../redux/store"; import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; -import { useDispatch } from "react-redux"; const useSocket = (bodyshop) => { const socketRef = useRef(null); const [clientId, setClientId] = useState(null); - const dispatch = useDispatch(); useEffect(() => { + const initializeSocket = async (token) => { + if (!bodyshop || !bodyshop.id) return; + + const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; + + const socketInstance = SocketIO(endpoint, { + path: "/wss", + withCredentials: true, + auth: { token }, + reconnectionAttempts: Infinity, + reconnectionDelay: 2000, + reconnectionDelayMax: 10000 + }); + + socketRef.current = socketInstance; + + // Handle socket events + const handleBodyshopMessage = (message) => { + if (!message || !message.type) return; + + switch (message.type) { + case "alert-update": + store.dispatch(addAlerts(message.payload)); + break; + default: + break; + } + + if (!import.meta.env.DEV) return; + console.log(`Received message for bodyshop ${bodyshop.id}:`, message); + }; + + const handleConnect = () => { + socketInstance.emit("join-bodyshop-room", bodyshop.id); + setClientId(socketInstance.id); + store.dispatch(setWssStatus("connected")); + }; + + const handleReconnect = () => { + store.dispatch(setWssStatus("connected")); + }; + + const handleConnectionError = (err) => { + console.error("Socket connection error:", err); + + // Handle token expiration + if (err.message.includes("auth/id-token-expired")) { + console.warn("Token expired, refreshing..."); + auth.currentUser?.getIdToken(true).then((newToken) => { + socketInstance.auth = { token: newToken }; // Update socket auth + socketInstance.connect(); // Retry connection + }); + } else { + store.dispatch(setWssStatus("error")); + } + }; + + const handleDisconnect = (reason) => { + console.warn("Socket disconnected:", reason); + store.dispatch(setWssStatus("disconnected")); + + // Manually trigger reconnection if necessary + if (!socketInstance.connected && reason !== "io server disconnect") { + setTimeout(() => { + if (socketInstance.disconnected) { + console.log("Manually triggering reconnection..."); + socketInstance.connect(); + } + }, 2000); // Retry after 2 seconds + } + }; + + // Register event handlers + socketInstance.on("connect", handleConnect); + socketInstance.on("reconnect", handleReconnect); + socketInstance.on("connect_error", handleConnectionError); + socketInstance.on("disconnect", handleDisconnect); + socketInstance.on("bodyshop-message", handleBodyshopMessage); + }; + const unsubscribe = auth.onIdTokenChanged(async (user) => { if (user) { - const newToken = await user.getIdToken(); + const token = await user.getIdToken(); if (socketRef.current) { - // Send new token to server - socketRef.current.emit("update-token", newToken); - } else if (bodyshop && bodyshop.id) { - // Initialize the socket - const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; - - const socketInstance = SocketIO(endpoint, { - path: "/wss", - withCredentials: true, - auth: { token: newToken }, - reconnectionAttempts: Infinity, - reconnectionDelay: 2000, - reconnectionDelayMax: 10000 - }); - - socketRef.current = socketInstance; - - const handleBodyshopMessage = (message) => { - if (!message || !message?.type) return; - - switch (message.type) { - case "alert-update": - store.dispatch(addAlerts(message.payload)); - break; - default: - break; - } - - if (!import.meta.env.DEV) return; - console.log(`Received message for bodyshop ${bodyshop.id}:`, message); - }; - - const handleConnect = () => { - socketInstance.emit("join-bodyshop-room", bodyshop.id); - setClientId(socketInstance.id); - store.dispatch(setWssStatus("connected")); - }; - - const handleReconnect = (attempt) => { - store.dispatch(setWssStatus("connected")); - }; - - const handleConnectionError = (err) => { - console.error("Socket connection error:", err); - store.dispatch(setWssStatus("error")); - }; - - const handleDisconnect = () => { - store.dispatch(setWssStatus("disconnected")); - }; - - socketInstance.on("connect", handleConnect); - socketInstance.on("reconnect", handleReconnect); - socketInstance.on("connect_error", handleConnectionError); - socketInstance.on("disconnect", handleDisconnect); - socketInstance.on("bodyshop-message", handleBodyshopMessage); + // Update token if socket exists + socketRef.current.emit("update-token", token); + } else { + // Initialize socket if not already connected + initializeSocket(token); } } else { // User is not authenticated @@ -82,7 +109,7 @@ const useSocket = (bodyshop) => { } }); - // Clean up the listener on unmount + // Clean up on unmount return () => { unsubscribe(); if (socketRef.current) { @@ -90,7 +117,7 @@ const useSocket = (bodyshop) => { socketRef.current = null; } }; - }, [bodyshop, dispatch]); + }, [bodyshop]); return { socket: socketRef.current, clientId }; }; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index a8e753f2f..90893a505 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -46,18 +46,25 @@ const redisSocketEvents = ({ // Token Update Events const registerUpdateEvents = (socket) => { + let latestTokenTimestamp = 0; + const updateToken = async (newToken) => { + const currentTimestamp = Date.now(); + latestTokenTimestamp = currentTimestamp; + try { - // noinspection UnnecessaryLocalVariableJS + // Verify token with Firebase Admin SDK const user = await admin.auth().verifyIdToken(newToken, true); + + // Skip outdated token validations + if (currentTimestamp < latestTokenTimestamp) { + createLogEvent(socket, "warn", "Outdated token validation skipped."); + return; + } + socket.user = user; - // If We ever want to persist user Data across workers - // await setSessionData(socket.id, "user", user); - - // Uncomment for further testing - // createLogEvent(socket, "debug", "Token updated successfully"); - + createLogEvent(socket, "debug", `Token updated successfully for socket ID: ${socket.id}`); socket.emit("token-updated", { success: true }); } catch (error) { if (error.code === "auth/id-token-expired") { @@ -66,14 +73,17 @@ const redisSocketEvents = ({ success: false, error: "Stale token." }); - } else { - createLogEvent(socket, "error", `Token update failed: ${error.message}`); - socket.emit("token-updated", { success: false, error: error.message }); - // For any other errors, optionally disconnect the socket - socket.disconnect(); + return; // Avoid disconnecting for expired tokens } + + createLogEvent(socket, "error", `Token update failed for socket ID: ${socket.id}, Error: ${error.message}`); + socket.emit("token-updated", { success: false, error: error.message }); + + // Optionally disconnect for invalid tokens or other errors + socket.disconnect(); } }; + socket.on("update-token", updateToken); };