feature/IO-3499-React-19 - Phone Number Formatter / Chat Open Button / Chat Affix container

This commit is contained in:
Dave
2026-01-23 15:50:38 -05:00
parent 53cb1d2f65
commit 7f43ba33f6
6 changed files with 82 additions and 55 deletions

View File

@@ -12,11 +12,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
const client = useApolloClient(); const client = useApolloClient();
const { socket } = useSocket(); const { socket } = useSocket();
// 1) FCM subscription (independent of socket handler registration) const messagingServicesId = bodyshop?.messagingservicesid;
useEffect(() => { const bodyshopId = bodyshop?.id;
if (!bodyshop?.messagingservicesid) return; const imexshopid = bodyshop?.imexshopid;
async function subscribeToTopicForFCMNotification() { const messagingEnabled = Boolean(messagingServicesId);
useEffect(() => {
if (!messagingEnabled) return;
(async () => {
try { try {
await requestForToken(); await requestForToken();
await axios.post("/notifications/subscribe", { await axios.post("/notifications/subscribe", {
@@ -24,23 +29,19 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
}), }),
type: "messaging", type: "messaging",
imexshopid: bodyshop.imexshopid imexshopid
}); });
} catch (error) { } catch (error) {
console.log("Error attempting to subscribe to messaging topic: ", error); console.log("Error attempting to subscribe to messaging topic: ", error);
} }
} })();
}, [messagingEnabled, imexshopid]);
subscribeToTopicForFCMNotification();
}, [bodyshop?.messagingservicesid, bodyshop?.imexshopid]);
// 2) Register socket handlers as soon as socket is connected (regardless of chatVisible)
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
if (!bodyshop?.messagingservicesid) return; if (!messagingEnabled) return;
if (!bodyshop?.id) return; if (!bodyshopId) return;
// If socket isn't connected yet, ensure no stale handlers remain.
if (!socket.connected) { if (!socket.connected) {
unregisterMessagingHandlers({ socket }); unregisterMessagingHandlers({ socket });
return; return;
@@ -56,16 +57,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
bodyshop bodyshop
}); });
return () => { return () => unregisterMessagingHandlers({ socket });
unregisterMessagingHandlers({ socket }); }, [socket, messagingEnabled, bodyshopId, client, currentUser?.email, bodyshop]);
};
}, [socket, socket?.connected, bodyshop?.id, bodyshop?.messagingservicesid, client, currentUser?.email]);
if (!bodyshop?.messagingservicesid) return <></>; if (!messagingEnabled) return null;
return ( return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop?.messagingservicesid ? <ChatPopupComponent /> : null} {messagingEnabled ? <ChatPopupComponent /> : null}
</div> </div>
); );
} }

View File

@@ -1,22 +1,23 @@
import { Button } from "antd";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors"; import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation searchingForConversation
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)) openChatByPhone: (payload) => dispatch(openChatByPhone(payload))
}); });
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) { export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) {
@@ -24,31 +25,59 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type
const { socket } = useSocket(); const { socket } = useSocket();
const notification = useNotification(); const notification = useNotification();
if (!phone) return <></>; if (!phone) return null;
if (!bodyshop.messagingservicesid) { const messagingEnabled = Boolean(bodyshop?.messagingservicesid);
return <PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>;
}
const parsed = useMemo(() => {
if (!messagingEnabled) return null;
try {
return parsePhoneNumber(phone, "CA") || null;
} catch {
return null;
}
}, [messagingEnabled, phone]);
const isValid = Boolean(parsed?.isValid?.() && parsed.isValid());
const clickable = messagingEnabled && !searchingForConversation && isValid;
const onClick = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
if (!messagingEnabled) return;
if (searchingForConversation) return;
if (!isValid) {
notification.error({ title: t("messaging.error.invalidphone") });
return;
}
openChatByPhone({
phone_num: parsed.formatInternational(),
jobid,
socket
});
},
[messagingEnabled, searchingForConversation, isValid, parsed, jobid, socket, openChatByPhone, notification, t]
);
const content = <PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>;
// If not clickable, render plain formatted text (no link styling)
if (!clickable) return content;
// Clickable: render as a link-styled button (best for a “command”)
return ( return (
<a <Button
href="# " type="link"
onClick={(e) => { onClick={onClick}
e.preventDefault(); className="chat-open-button-link"
e.stopPropagation(); aria-label={t("messaging.actions.openchat") || "Open chat"}
if (searchingForConversation) return; // Prevent finding the same thing twice.
const p = parsePhoneNumber(phone, "CA");
if (p && p.isValid()) {
openChatByPhone({ phone_num: p.formatInternational(), jobid, socket });
} else {
notification.error({ title: t("messaging.error.invalidphone") });
}
}}
> >
<PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter> {content}
</a> </Button>
); );
} }

View File

@@ -2431,7 +2431,8 @@
"messaging": { "messaging": {
"actions": { "actions": {
"link": "Link to Job", "link": "Link to Job",
"new": "New Conversation" "new": "New Conversation",
"openchat": "Open Chat"
}, },
"errors": { "errors": {
"invalidphone": "The phone number is invalid. Unable to open conversation. ", "invalidphone": "The phone number is invalid. Unable to open conversation. ",

View File

@@ -2428,7 +2428,8 @@
"messaging": { "messaging": {
"actions": { "actions": {
"link": "", "link": "",
"new": "" "new": "",
"openchat": ""
}, },
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",

View File

@@ -2428,7 +2428,9 @@
"messaging": { "messaging": {
"actions": { "actions": {
"link": "", "link": "",
"new": "" "new": "",
"openchat": ""
}, },
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",

View File

@@ -11,13 +11,8 @@ export default function PhoneNumberFormatter({ children, type }) {
return ( return (
<span> <span>
<Text>{phone}</Text> <span>{phone}</span>
{type ? ( {type ? <Text type="secondary"> ({type})</Text> : null}
<>
{" "}
<Text type="secondary">({type})</Text>
</>
) : null}
</span> </span>
); );
} }