feature/IO-3000-messaging-sockets-migrations2 -
- Fix Chat Icon logger error - Fix Socket Robustness - added additional wss status for error - Installed ant-design icons Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
1
client/package-lock.json
generated
1
client/package-lock.json
generated
@@ -85,6 +85,7 @@
|
|||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^1.14.1",
|
"@dotenvx/dotenvx": "^1.14.1",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^1.14.1",
|
"@dotenvx/dotenvx": "^1.14.1",
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import React, { useEffect, useRef } from "react";
|
||||||
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 { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { renderMessage } from "./renderMessage";
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
|
|
||||||
export default function ChatMessageListComponent({ messages }) {
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
@@ -21,7 +16,7 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 100); // Delay of 100ms to allow rendering
|
}, 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
|
}, [messages.length]); // Run only once on component mount
|
||||||
|
|
||||||
// Scroll to the bottom after the new messages are rendered
|
// 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
|
}, 50); // Slight delay to ensure layout recalculates
|
||||||
}
|
}
|
||||||
}, [messages]); // Triggered when new messages are added
|
}, [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 (
|
|
||||||
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
|
||||||
<div className="message msgmargin">
|
|
||||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
|
||||||
<div>
|
|
||||||
{message.image_path &&
|
|
||||||
message.image_path.map((i, idx) => (
|
|
||||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
|
||||||
<a href={i} target="__blank" rel="noopener noreferrer">
|
|
||||||
<img alt="Received" className="message-img" src={i} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div>{message.text}</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{message.status && (
|
|
||||||
<div className="message-status">
|
|
||||||
<Icon
|
|
||||||
component={message.status === "sent" ? MdDone : message.status === "delivered" ? MdDoneAll : null}
|
|
||||||
className="message-icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{message.isoutbound && (
|
|
||||||
<div style={{ fontSize: 10 }}>
|
|
||||||
{i18n.t("messaging.labels.sentby", {
|
|
||||||
by: message.userid,
|
|
||||||
time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a")
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={messages}
|
data={messages}
|
||||||
itemContent={(index) => renderMessage(index)}
|
itemContent={(index) => renderMessage(messages, index)} // Pass `messages` to renderMessage
|
||||||
followOutput="smooth" // Ensure smooth scrolling when new data is appended
|
followOutput="smooth" // Ensure smooth scrolling when new data is appended
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
42
client/src/components/chat-messages-list/renderMessage.jsx
Normal file
42
client/src/components/chat-messages-list/renderMessage.jsx
Normal file
@@ -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 (
|
||||||
|
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
||||||
|
<div className="message msgmargin">
|
||||||
|
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||||
|
<div>
|
||||||
|
{message.image_path &&
|
||||||
|
message.image_path.map((i, idx) => (
|
||||||
|
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<a href={i} target="__blank" rel="noopener noreferrer">
|
||||||
|
<img alt="Received" className="message-img" src={i} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>{message.text}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
||||||
|
<div className="message-status">
|
||||||
|
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{message.isoutbound && (
|
||||||
|
<div style={{ fontSize: 10 }}>
|
||||||
|
{i18n.t("messaging.labels.sentby", {
|
||||||
|
by: message.userid,
|
||||||
|
time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a")
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,18 +1,33 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { GlobalOutlined } from "@ant-design/icons";
|
import { GlobalOutlined, WarningOutlined } from "@ant-design/icons";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { selectWssStatus } from "../../redux/application/application.selectors";
|
import { selectWssStatus } from "../../redux/application/application.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
wssStatus: selectWssStatus
|
wssStatus: selectWssStatus
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
|
|
||||||
|
|
||||||
export function WssStatusDisplay({ wssStatus }) {
|
export function WssStatusDisplay({ wssStatus }) {
|
||||||
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
|
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
|
||||||
return <GlobalOutlined style={{ color: wssStatus === "connected" ? "green" : "red", marginRight: ".5rem" }} />;
|
|
||||||
|
let icon;
|
||||||
|
let color;
|
||||||
|
|
||||||
|
if (wssStatus === "connected") {
|
||||||
|
icon = <GlobalOutlined />;
|
||||||
|
color = "green";
|
||||||
|
} else if (wssStatus === "error") {
|
||||||
|
icon = <WarningOutlined />;
|
||||||
|
color = "red";
|
||||||
|
} else {
|
||||||
|
icon = <GlobalOutlined />;
|
||||||
|
color = "gray"; // Default for other statuses like "disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span style={{ color, marginRight: ".5rem" }}>{icon}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
|
||||||
|
|||||||
@@ -3,75 +3,102 @@ import SocketIO from "socket.io-client";
|
|||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
import { store } from "../../redux/store";
|
import { store } from "../../redux/store";
|
||||||
import { addAlerts, setWssStatus } from "../../redux/application/application.actions";
|
import { addAlerts, setWssStatus } from "../../redux/application/application.actions";
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
const useSocket = (bodyshop) => {
|
const useSocket = (bodyshop) => {
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
const [clientId, setClientId] = useState(null);
|
const [clientId, setClientId] = useState(null);
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
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) => {
|
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
const newToken = await user.getIdToken();
|
const token = await user.getIdToken();
|
||||||
|
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
// Send new token to server
|
// Update token if socket exists
|
||||||
socketRef.current.emit("update-token", newToken);
|
socketRef.current.emit("update-token", token);
|
||||||
} else if (bodyshop && bodyshop.id) {
|
} else {
|
||||||
// Initialize the socket
|
// Initialize socket if not already connected
|
||||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
initializeSocket(token);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User is not authenticated
|
// User is not authenticated
|
||||||
@@ -82,7 +109,7 @@ const useSocket = (bodyshop) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up the listener on unmount
|
// Clean up on unmount
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
@@ -90,7 +117,7 @@ const useSocket = (bodyshop) => {
|
|||||||
socketRef.current = null;
|
socketRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [bodyshop, dispatch]);
|
}, [bodyshop]);
|
||||||
|
|
||||||
return { socket: socketRef.current, clientId };
|
return { socket: socketRef.current, clientId };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,18 +46,25 @@ const redisSocketEvents = ({
|
|||||||
|
|
||||||
// Token Update Events
|
// Token Update Events
|
||||||
const registerUpdateEvents = (socket) => {
|
const registerUpdateEvents = (socket) => {
|
||||||
|
let latestTokenTimestamp = 0;
|
||||||
|
|
||||||
const updateToken = async (newToken) => {
|
const updateToken = async (newToken) => {
|
||||||
|
const currentTimestamp = Date.now();
|
||||||
|
latestTokenTimestamp = currentTimestamp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// noinspection UnnecessaryLocalVariableJS
|
// Verify token with Firebase Admin SDK
|
||||||
const user = await admin.auth().verifyIdToken(newToken, true);
|
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;
|
socket.user = user;
|
||||||
|
|
||||||
// If We ever want to persist user Data across workers
|
createLogEvent(socket, "debug", `Token updated successfully for socket ID: ${socket.id}`);
|
||||||
// await setSessionData(socket.id, "user", user);
|
|
||||||
|
|
||||||
// Uncomment for further testing
|
|
||||||
// createLogEvent(socket, "debug", "Token updated successfully");
|
|
||||||
|
|
||||||
socket.emit("token-updated", { success: true });
|
socket.emit("token-updated", { success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "auth/id-token-expired") {
|
if (error.code === "auth/id-token-expired") {
|
||||||
@@ -66,14 +73,17 @@ const redisSocketEvents = ({
|
|||||||
success: false,
|
success: false,
|
||||||
error: "Stale token."
|
error: "Stale token."
|
||||||
});
|
});
|
||||||
} else {
|
return; // Avoid disconnecting for expired tokens
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
socket.on("update-token", updateToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user