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) => (
-
- ))}
-
{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) => (
+
+ ))}
+
{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);
};