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 { socket } = useSocket();
// 1) FCM subscription (independent of socket handler registration)
useEffect(() => {
if (!bodyshop?.messagingservicesid) return;
const messagingServicesId = bodyshop?.messagingservicesid;
const bodyshopId = bodyshop?.id;
const imexshopid = bodyshop?.imexshopid;
async function subscribeToTopicForFCMNotification() {
const messagingEnabled = Boolean(messagingServicesId);
useEffect(() => {
if (!messagingEnabled) return;
(async () => {
try {
await requestForToken();
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
}),
type: "messaging",
imexshopid: bodyshop.imexshopid
imexshopid
});
} catch (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(() => {
if (!socket) return;
if (!bodyshop?.messagingservicesid) return;
if (!bodyshop?.id) return;
if (!messagingEnabled) return;
if (!bodyshopId) return;
// If socket isn't connected yet, ensure no stale handlers remain.
if (!socket.connected) {
unregisterMessagingHandlers({ socket });
return;
@@ -56,16 +57,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
bodyshop
});
return () => {
unregisterMessagingHandlers({ socket });
};
}, [socket, socket?.connected, bodyshop?.id, bodyshop?.messagingservicesid, client, currentUser?.email]);
return () => unregisterMessagingHandlers({ socket });
}, [socket, messagingEnabled, bodyshopId, client, currentUser?.email, bodyshop]);
if (!bodyshop?.messagingservicesid) return <></>;
if (!messagingEnabled) return null;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop?.messagingservicesid ? <ChatPopupComponent /> : null}
{messagingEnabled ? <ChatPopupComponent /> : null}
</div>
);
}

View File

@@ -1,22 +1,23 @@
import { Button } from "antd";
import parsePhoneNumber from "libphonenumber-js";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation
searchingForConversation
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
openChatByPhone: (payload) => dispatch(openChatByPhone(payload))
});
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) {
@@ -24,31 +25,59 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type
const { socket } = useSocket();
const notification = useNotification();
if (!phone) return <></>;
if (!phone) return null;
if (!bodyshop.messagingservicesid) {
return <PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>;
}
const messagingEnabled = Boolean(bodyshop?.messagingservicesid);
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 (
<a
href="# "
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
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") });
}
}}
<Button
type="link"
onClick={onClick}
className="chat-open-button-link"
aria-label={t("messaging.actions.openchat") || "Open chat"}
>
<PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>
</a>
{content}
</Button>
);
}

View File

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

View File

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

View File

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

View File

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