Compare commits
6 Commits
rrScratch1
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bed87174d4 | ||
|
|
03ae7bb160 | ||
|
|
6e6c44f2b9 | ||
|
|
ae0bfad89a | ||
|
|
27de849be7 | ||
|
|
1309d8ff65 |
@@ -1,7 +1,7 @@
|
||||
import { Badge, Card, List, Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
@@ -25,12 +25,7 @@ function ChatConversationListComponent({
|
||||
setSelectedConversation,
|
||||
loadMoreConversations
|
||||
}) {
|
||||
const cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: 60
|
||||
});
|
||||
|
||||
const rowRenderer = ({ index, key, style, parent }) => {
|
||||
const renderConversation = (index) => {
|
||||
const item = conversationList[index];
|
||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||
const cardContentLeft =
|
||||
@@ -52,6 +47,7 @@ function ChatConversationListComponent({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
||||
|
||||
const getCardStyle = () =>
|
||||
@@ -60,40 +56,27 @@ function ChatConversationListComponent({
|
||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
||||
|
||||
return (
|
||||
<CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
style={style}
|
||||
className={`chat-list-item
|
||||
${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
|
||||
>
|
||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
</CellMeasurer>
|
||||
<List.Item
|
||||
key={item.id}
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||
>
|
||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-list-container">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<VirtualizedList
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={conversationList.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
||||
if (scrollTop + clientHeight === scrollHeight) {
|
||||
loadMoreConversations();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<Virtuoso
|
||||
data={conversationList}
|
||||
itemContent={(index) => renderConversation(index)}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.chat-list-container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
height: 100%; /* Ensure it takes up the full available height */
|
||||
border: 1px solid gainsboro;
|
||||
overflow: auto; /* Allow scrolling for the Virtuoso component */
|
||||
}
|
||||
|
||||
.chat-list-item {
|
||||
@@ -14,3 +14,24 @@
|
||||
color: #ff7a00;
|
||||
}
|
||||
}
|
||||
|
||||
/* Virtuoso item container adjustments */
|
||||
.chat-list-container > div {
|
||||
height: 100%; /* Ensure Virtuoso takes full height */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Add spacing and better alignment for items */
|
||||
.chat-list-item {
|
||||
padding: 0.5rem 0; /* Add spacing between list items */
|
||||
|
||||
.ant-card {
|
||||
border-radius: 8px; /* Slight rounding for card edges */
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Subtle shadow for better definition */
|
||||
}
|
||||
|
||||
&:hover .ant-card {
|
||||
border-color: #ff7a00; /* Highlight border on hover */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,83 @@
|
||||
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
import axios from "axios";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||
const {
|
||||
loading: convoLoading,
|
||||
error: convoError,
|
||||
data: convoData
|
||||
} = useQuery(GET_CONVERSATION_DETAILS, {
|
||||
variables: { conversationId: selectedConversation },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||||
variables: { conversationId: selectedConversation }
|
||||
});
|
||||
|
||||
const { socket } = useContext(SocketContext);
|
||||
const [conversationDetails, setConversationDetails] = useState({});
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error] = useState(null);
|
||||
|
||||
const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
|
||||
variables: { conversationId: selectedConversation },
|
||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({
|
||||
__typename: "conversations",
|
||||
id: selectedConversation
|
||||
}),
|
||||
fields: {
|
||||
messages_aggregate(cached) {
|
||||
return { aggregate: { count: 0 } };
|
||||
}
|
||||
}
|
||||
// Fetch conversation details and messages when a conversation is selected
|
||||
useEffect(() => {
|
||||
if (socket && selectedConversation) {
|
||||
setLoading(true);
|
||||
socket.emit("join-conversation", selectedConversation);
|
||||
|
||||
socket.on("conversation-details", (data) => {
|
||||
setConversationDetails(data.conversation);
|
||||
setMessages(data.messages);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
socket.on("new-message", (message) => {
|
||||
setMessages((prevMessages) => [...prevMessages, message]);
|
||||
});
|
||||
|
||||
socket.on("conversation-list-updated", (data) => {
|
||||
setConversationDetails(data.conversation);
|
||||
setMessages(data.messages);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.emit("leave-conversation", selectedConversation);
|
||||
socket.off("conversation-details");
|
||||
socket.off("new-message");
|
||||
socket.off("conversation-list-updated");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const unreadCount =
|
||||
data &&
|
||||
data.messages &&
|
||||
data.messages.reduce((acc, val) => {
|
||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||
}, 0);
|
||||
}, [socket, selectedConversation]);
|
||||
|
||||
// Mark messages as read
|
||||
const handleMarkConversationAsRead = async () => {
|
||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||
if (messages.some((msg) => !msg.read) && !markingAsReadInProgress) {
|
||||
setMarkingAsReadInProgress(true);
|
||||
await markConversationRead({});
|
||||
await axios.post("/sms/markConversationRead", {
|
||||
conversationid: selectedConversation,
|
||||
imexshopid: bodyshop.imexshopid
|
||||
// Emit a WebSocket event to mark messages as read
|
||||
socket.emit("mark-as-read", {
|
||||
conversationId: selectedConversation,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
bodyshopId: bodyshop.id
|
||||
});
|
||||
|
||||
setMarkingAsReadInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!messages || !conversationDetails || !messages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatConversationComponent
|
||||
subState={[loading || convoLoading, error || convoError]}
|
||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||
messages={data ? data.messages : []}
|
||||
subState={[loading, error]}
|
||||
conversation={conversationDetails}
|
||||
messages={messages}
|
||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationContainer);
|
||||
|
||||
@@ -2,105 +2,90 @@ import Icon from "@ant-design/icons";
|
||||
import { Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import "./chat-message-list.styles.scss";
|
||||
|
||||
export default function ChatMessageListComponent({ messages }) {
|
||||
const virtualizedListRef = useRef(null);
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
const _cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
// minHeight: 50,
|
||||
defaultHeight: 100
|
||||
});
|
||||
// Scroll to the bottom after a short delay when the component mounts
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: messages.length - 1,
|
||||
behavior: "auto" // Instantly scroll to the bottom
|
||||
});
|
||||
}
|
||||
}, 100); // Delay of 100ms to allow rendering
|
||||
return () => clearTimeout(timer); // Cleanup the timer on unmount
|
||||
}, [messages.length]); // Run only once on component mount
|
||||
|
||||
const scrollToBottom = (renderedrows) => {
|
||||
//console.log("Scrolling to", messages.length);
|
||||
// !!virtualizedListRef.current &&
|
||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
||||
//Scrolling does not work on this version of React.
|
||||
};
|
||||
// Scroll to the bottom after the new messages are rendered
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current) {
|
||||
// Allow the DOM and Virtuoso to fully render the new data
|
||||
setTimeout(() => {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: messages.length - 1,
|
||||
align: "end", // Ensure the last message is fully visible
|
||||
behavior: "smooth" // Smooth scrolling
|
||||
});
|
||||
}, 50); // Slight delay to ensure layout recalculates
|
||||
}
|
||||
}, [messages]); // Triggered when new messages are added
|
||||
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
|
||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
||||
const renderMessage = (index) => {
|
||||
const message = messages[index];
|
||||
return (
|
||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div
|
||||
ref={registerChild}
|
||||
onLoad={measure}
|
||||
style={style}
|
||||
className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
|
||||
>
|
||||
<div className="message msgmargin">
|
||||
{MessageRender(messages[index])}
|
||||
{StatusRender(messages[index].status)}
|
||||
<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>
|
||||
{messages[index].isoutbound && (
|
||||
<div style={{ fontSize: 10 }}>
|
||||
{i18n.t("messaging.labels.sentby", {
|
||||
by: messages[index].userid,
|
||||
time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
|
||||
})}
|
||||
</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>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
ref={virtualizedListRef}
|
||||
width={width}
|
||||
height={height}
|
||||
rowHeight={_cache.rowHeight}
|
||||
rowRenderer={_rowRenderer}
|
||||
rowCount={messages.length}
|
||||
overscanRowCount={10}
|
||||
estimatedRowSize={150}
|
||||
scrollToIndex={messages.length}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={(index) => renderMessage(index)}
|
||||
followOutput="smooth" // Ensure smooth scrolling when new data is appended
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MessageRender = (message) => {
|
||||
return (
|
||||
<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">
|
||||
<img alt="Received" className="message-img" src={i} />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<div>{message.text}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusRender = (status) => {
|
||||
switch (status) {
|
||||
case "sent":
|
||||
return <Icon component={MdDone} className="message-icon" />;
|
||||
case "delivered":
|
||||
return <Icon component={MdDoneAll} className="message-icon" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
.message-icon {
|
||||
//position: absolute;
|
||||
// bottom: 0rem;
|
||||
color: whitesmoke;
|
||||
border: #000000;
|
||||
position: absolute;
|
||||
margin: 0 0.1rem;
|
||||
bottom: 0.1rem;
|
||||
right: 0.3rem;
|
||||
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.chat {
|
||||
flex: 1;
|
||||
//width: 300px;
|
||||
//border: solid 1px #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.8rem 0rem;
|
||||
overflow: hidden; // Ensure the content scrolls correctly
|
||||
}
|
||||
|
||||
.messages {
|
||||
//margin-top: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem; // Add padding to avoid edge clipping
|
||||
}
|
||||
|
||||
.message {
|
||||
border-radius: 20px;
|
||||
padding: 0.25rem 0.8rem;
|
||||
//margin-top: 5px;
|
||||
// margin-bottom: 5px;
|
||||
//display: inline-block;
|
||||
|
||||
.message-img {
|
||||
max-width: 10rem;
|
||||
@@ -56,7 +49,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.yours .message.last:before {
|
||||
.yours .message:last-child:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
@@ -68,7 +61,7 @@
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
.yours .message.last:after {
|
||||
.yours .message:last-child:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
@@ -88,12 +81,11 @@
|
||||
color: white;
|
||||
margin-left: 25%;
|
||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||
background-attachment: fixed;
|
||||
position: relative;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.mine .message.last:before {
|
||||
.mine .message:last-child:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
@@ -102,11 +94,10 @@
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||
background-attachment: fixed;
|
||||
border-bottom-left-radius: 15px;
|
||||
}
|
||||
|
||||
.mine .message.last:after {
|
||||
.mine .message:last-child:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,73 +1,59 @@
|
||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined } from "@ant-design/icons";
|
||||
import { Badge, Card, Col, Row, Space, Tooltip, Typography } from "antd";
|
||||
import React, { useCallback, useContext, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import {
|
||||
selectChatVisible,
|
||||
selectConversations,
|
||||
selectSelectedConversation,
|
||||
selectUnreadCount
|
||||
} from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import "./chat-popup.styles.scss";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
chatVisible: selectChatVisible
|
||||
chatVisible: selectChatVisible,
|
||||
bodyshop: selectBodyshop,
|
||||
conversations: selectConversations,
|
||||
unreadCount: selectUnreadCount
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||
});
|
||||
|
||||
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
||||
export function ChatPopupComponent({
|
||||
chatVisible,
|
||||
selectedConversation,
|
||||
toggleChatVisible,
|
||||
bodyshop,
|
||||
conversations,
|
||||
unreadCount
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [pollInterval, setpollInterval] = useState(0);
|
||||
|
||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
...(pollInterval > 0 ? { pollInterval } : {})
|
||||
});
|
||||
|
||||
const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !chatVisible,
|
||||
...(pollInterval > 0 ? { pollInterval } : {})
|
||||
});
|
||||
|
||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||
const { socket } = useContext(SocketContext);
|
||||
|
||||
// Emit event to open messaging when chat becomes visible
|
||||
useEffect(() => {
|
||||
if (fcmToken) {
|
||||
setpollInterval(0);
|
||||
} else {
|
||||
setpollInterval(90000);
|
||||
if (chatVisible && socket && bodyshop?.id) {
|
||||
socket.emit("open-messaging", bodyshop.id);
|
||||
}
|
||||
}, [fcmToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatVisible)
|
||||
getConversations({
|
||||
variables: {
|
||||
offset: 0
|
||||
}
|
||||
});
|
||||
}, [chatVisible, getConversations]);
|
||||
}, [chatVisible, socket, bodyshop?.id]);
|
||||
|
||||
// Handle loading more conversations
|
||||
const loadMoreConversations = useCallback(() => {
|
||||
if (data)
|
||||
fetchMore({
|
||||
variables: {
|
||||
offset: data.conversations.length
|
||||
}
|
||||
});
|
||||
}, [data, fetchMore]);
|
||||
|
||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||
if (socket) {
|
||||
socket.emit("load-more-conversations", { offset: conversations.length });
|
||||
}
|
||||
}, [socket, conversations.length]);
|
||||
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
@@ -80,21 +66,19 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
||||
<Tooltip title={t("messaging.labels.recentonly")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
|
||||
{pollInterval > 0 && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
</Space>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
{loading ? (
|
||||
{conversations && conversations.length === 0 ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<ChatConversationListComponent
|
||||
conversationList={data ? data.conversations : []}
|
||||
conversationList={conversations.conversations}
|
||||
loadMoreConversations={loadMoreConversations}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,55 +1,66 @@
|
||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Input, Spin } from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { sendMessage, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
isSending: selectIsSending,
|
||||
message: selectMessage
|
||||
message: selectMessage,
|
||||
user: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
sendMessage: (message) => dispatch(sendMessage(message)),
|
||||
setMessage: (message) => dispatch(setMessage(message))
|
||||
});
|
||||
|
||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||
function ChatSendMessageComponent({ conversation, bodyshop, isSending, message, setMessage, user }) {
|
||||
const { socket } = useContext(SocketContext); // Access WebSocket instance
|
||||
const inputArea = useRef(null);
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = () => {
|
||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||
|
||||
logImEXEvent("messaging_send_message");
|
||||
|
||||
if (selectedImages.length < 11) {
|
||||
sendMessage({
|
||||
const messageData = {
|
||||
user,
|
||||
to: conversation.phone_num,
|
||||
body: message || "",
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
selectedMedia: selectedImages,
|
||||
imexshopid: bodyshop.imexshopid
|
||||
});
|
||||
};
|
||||
|
||||
// Emit the send-message event via WebSocket
|
||||
socket.emit("send-message", messageData);
|
||||
|
||||
setSelectedMedia(
|
||||
selectedMedia.map((i) => {
|
||||
return { ...i, isSelected: false };
|
||||
})
|
||||
);
|
||||
|
||||
// Optionally clear the input message
|
||||
setMessage("");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,15 +85,11 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
if (!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<SendOutlined
|
||||
className="imex-flex-row__margin"
|
||||
// disabled={message === "" || !message}
|
||||
onClick={handleEnter}
|
||||
/>
|
||||
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
||||
<Spin
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
indicator={
|
||||
|
||||
@@ -3,10 +3,12 @@ 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 unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||
@@ -45,14 +47,12 @@ const useSocket = (bodyshop) => {
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
console.log("Socket connected:", socketInstance.id);
|
||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||
setClientId(socketInstance.id);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleReconnect = (attempt) => {
|
||||
console.log(`Socket reconnected after ${attempt} attempts`);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
@@ -62,15 +62,30 @@ const useSocket = (bodyshop) => {
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
console.log("Socket disconnected");
|
||||
store.dispatch(setWssStatus("disconnected"));
|
||||
};
|
||||
|
||||
const handleMessagingList = (data) => {
|
||||
dispatch({ type: "SET_CONVERSATIONS", payload: data.conversations });
|
||||
};
|
||||
|
||||
const handleNewMessage = (message) => {
|
||||
dispatch({ type: "ADD_MESSAGE", payload: message });
|
||||
};
|
||||
|
||||
const handleNewConversation = (data) => {
|
||||
dispatch({ type: "ADD_CONVERSATION", payload: data.conversation });
|
||||
dispatch({ type: "ADD_MESSAGE", payload: data.message });
|
||||
};
|
||||
|
||||
socketInstance.on("messaging-list", handleMessagingList);
|
||||
socketInstance.on("new-message", handleNewMessage);
|
||||
socketInstance.on("connect", handleConnect);
|
||||
socketInstance.on("reconnect", handleReconnect);
|
||||
socketInstance.on("connect_error", handleConnectionError);
|
||||
socketInstance.on("disconnect", handleDisconnect);
|
||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||
socketInstance.on("new-conversation", handleNewConversation);
|
||||
}
|
||||
} else {
|
||||
// User is not authenticated
|
||||
@@ -89,7 +104,7 @@ const useSocket = (bodyshop) => {
|
||||
socketRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [bodyshop]);
|
||||
}, [bodyshop, dispatch]);
|
||||
|
||||
return { socket: socketRef.current, clientId };
|
||||
};
|
||||
|
||||
@@ -114,3 +114,22 @@ export const UPDATE_CONVERSATION_LABEL = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CONVERSATION_MESSAGES = gql`
|
||||
query GET_CONVERSATION_MESSAGES($conversationId: uuid!) {
|
||||
conversation: conversations_by_pk(id: $conversationId) {
|
||||
id
|
||||
phone_num
|
||||
updated_at
|
||||
label
|
||||
}
|
||||
messages: messages(where: { conversationid: { _eq: $conversationId } }, order_by: { created_at: asc }) {
|
||||
id
|
||||
text
|
||||
created_at
|
||||
read
|
||||
isoutbound
|
||||
userid
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -145,7 +145,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
};
|
||||
|
||||
fetchAlerts();
|
||||
}, []);
|
||||
}, [setAlerts]);
|
||||
|
||||
// Use useEffect to watch for new alerts
|
||||
useEffect(() => {
|
||||
@@ -647,7 +647,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
||||
{true && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
||||
<Layout style={{ minHeight: "100vh" }} className="layout-container">
|
||||
<UpdateAlert />
|
||||
<HeaderContainer />
|
||||
|
||||
@@ -2,9 +2,9 @@ import MessagingActionTypes from "./messaging.types";
|
||||
|
||||
export const toggleChatVisible = () => ({
|
||||
type: MessagingActionTypes.TOGGLE_CHAT_VISIBLE
|
||||
//payload: user
|
||||
});
|
||||
|
||||
// Action to handle when a message is sent via WebSocket
|
||||
export const sendMessage = (message) => ({
|
||||
type: MessagingActionTypes.SEND_MESSAGE,
|
||||
payload: message
|
||||
@@ -19,17 +19,45 @@ export const sendMessageFailure = (error) => ({
|
||||
type: MessagingActionTypes.SEND_MESSAGE_FAILURE,
|
||||
payload: error
|
||||
});
|
||||
|
||||
// Set the selected conversation by ID
|
||||
export const setSelectedConversation = (conversationId) => ({
|
||||
type: MessagingActionTypes.SET_SELECTED_CONVERSATION,
|
||||
payload: conversationId
|
||||
});
|
||||
|
||||
// Open chat by phone number (if there’s a need to search for a conversation)
|
||||
export const openChatByPhone = (phoneNumber) => ({
|
||||
type: MessagingActionTypes.OPEN_CHAT_BY_PHONE,
|
||||
payload: phoneNumber
|
||||
});
|
||||
|
||||
// Set an individual message (e.g., from a new message event)
|
||||
export const setMessage = (message) => ({
|
||||
type: MessagingActionTypes.SET_MESSAGE,
|
||||
payload: message
|
||||
});
|
||||
|
||||
// Set the list of conversations received from WebSocket
|
||||
export const setConversations = (conversations) => ({
|
||||
type: MessagingActionTypes.SET_CONVERSATIONS,
|
||||
payload: conversations
|
||||
});
|
||||
|
||||
// Add a message to the conversation messages
|
||||
export const addMessage = (message) => ({
|
||||
type: MessagingActionTypes.ADD_MESSAGE,
|
||||
payload: message
|
||||
});
|
||||
|
||||
// Add a Conversation to the list of conversations
|
||||
export const addConversation = (conversation) => ({
|
||||
type: MessagingActionTypes.ADD_CONVERSATION,
|
||||
payload: conversation
|
||||
});
|
||||
|
||||
// Update unread count for a conversation (e.g., after marking messages as read)
|
||||
export const updateUnreadCount = (conversationId, unreadCount) => ({
|
||||
type: MessagingActionTypes.UPDATE_UNREAD_COUNT,
|
||||
payload: { conversationId, unreadCount }
|
||||
});
|
||||
|
||||
@@ -6,6 +6,11 @@ const INITIAL_STATE = {
|
||||
isSending: false,
|
||||
error: null,
|
||||
message: null,
|
||||
conversations: {
|
||||
conversations: []
|
||||
},
|
||||
messages: [],
|
||||
unreadcnt: 0,
|
||||
searchingForConversation: false
|
||||
};
|
||||
|
||||
@@ -13,39 +18,74 @@ const messagingReducer = (state = INITIAL_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case MessagingActionTypes.SET_MESSAGE:
|
||||
return { ...state, message: action.payload };
|
||||
|
||||
case MessagingActionTypes.TOGGLE_CHAT_VISIBLE:
|
||||
return {
|
||||
...state,
|
||||
open: !state.open
|
||||
};
|
||||
|
||||
case MessagingActionTypes.OPEN_CHAT_BY_PHONE:
|
||||
return {
|
||||
...state,
|
||||
searchingForConversation: true
|
||||
};
|
||||
|
||||
case MessagingActionTypes.SET_SELECTED_CONVERSATION:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
searchingForConversation: false,
|
||||
selectedConversationId: action.payload
|
||||
selectedConversationId: action.payload,
|
||||
messages: [] // Reset messages when a new conversation is selected
|
||||
};
|
||||
|
||||
case MessagingActionTypes.SEND_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
isSending: true
|
||||
};
|
||||
|
||||
case MessagingActionTypes.SEND_MESSAGE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
message: "",
|
||||
isSending: false
|
||||
};
|
||||
|
||||
case MessagingActionTypes.SEND_MESSAGE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
error: action.payload
|
||||
error: action.payload,
|
||||
isSending: false
|
||||
};
|
||||
|
||||
case MessagingActionTypes.SET_CONVERSATIONS:
|
||||
return {
|
||||
...state,
|
||||
conversations: action.payload
|
||||
};
|
||||
|
||||
case MessagingActionTypes.ADD_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
messages: [...state.messages, action.payload]
|
||||
};
|
||||
|
||||
case MessagingActionTypes.ADD_CONVERSATION:
|
||||
return {
|
||||
...state,
|
||||
conversations: {
|
||||
...state.conversations,
|
||||
conversations: [
|
||||
...(state.conversations.conversations || []),
|
||||
{
|
||||
...action.payload, // Ensure all fields from payload are included
|
||||
messages_aggregate: action.payload.messages_aggregate || { aggregate: { count: 0 } }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -19,3 +19,8 @@ export const searchingForConversation = createSelector(
|
||||
[selectMessaging],
|
||||
(messaging) => messaging.searchingForConversation
|
||||
);
|
||||
|
||||
// New selectors for conversations and unread count
|
||||
export const selectConversations = createSelector([selectMessaging], (messaging) => messaging.conversations);
|
||||
|
||||
export const selectUnreadCount = createSelector([selectMessaging], (messaging) => messaging.unreadCount);
|
||||
|
||||
@@ -5,6 +5,10 @@ const MessagingActionTypes = {
|
||||
SEND_MESSAGE_FAILURE: "SEND_MESSAGE_FAILURE",
|
||||
SET_SELECTED_CONVERSATION: "SET_SELECTED_CONVERSATION",
|
||||
OPEN_CHAT_BY_PHONE: "OPEN_CHAT_BY_PHONE",
|
||||
SET_MESSAGE: "SET_MESSAGE"
|
||||
SET_MESSAGE: "SET_MESSAGE",
|
||||
ADD_MESSAGE: "ADD_MESSAGE",
|
||||
UPDATE_UNREAD_COUNT: "UPDATE_UNREAD_COUNT",
|
||||
SET_CONVERSATIONS: "SET_CONVERSATIONS",
|
||||
ADD_CONVERSATION: "ADD_CONVERSATION"
|
||||
};
|
||||
export default MessagingActionTypes;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { promises as fsPromises } from "fs";
|
||||
import { createRequire } from "module";
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
import { createLogger, defineConfig } from "vite";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import eslint from "vite-plugin-eslint";
|
||||
@@ -18,28 +15,6 @@ process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||
const getFormattedTimestamp = () =>
|
||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||
|
||||
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
|
||||
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
|
||||
|
||||
function reactVirtualizedFix() {
|
||||
return {
|
||||
name: "flat:react-virtualized",
|
||||
configResolved: async () => {
|
||||
const require = createRequire(import.meta.url);
|
||||
const reactVirtualizedPath = require.resolve("react-virtualized");
|
||||
const { pathname: reactVirtualizedFilePath } = new url.URL(reactVirtualizedPath, import.meta.url);
|
||||
const file = reactVirtualizedFilePath.replace(
|
||||
path.join("dist", "commonjs", "index.js"),
|
||||
path.join("dist", "es", "WindowScroller", "utils", "onScroll.js")
|
||||
);
|
||||
const code = await fsPromises.readFile(file, "utf-8");
|
||||
const modified = code.replace(WRONG_CODE, "");
|
||||
await fsPromises.writeFile(file, modified);
|
||||
}
|
||||
};
|
||||
}
|
||||
/** End of hack */
|
||||
|
||||
export const logger = createLogger("info", {
|
||||
allowClearScreen: false
|
||||
});
|
||||
@@ -108,7 +83,6 @@ export default defineConfig({
|
||||
gcm_sender_id: "103953800507"
|
||||
}
|
||||
}),
|
||||
reactVirtualizedFix(),
|
||||
react(),
|
||||
eslint()
|
||||
],
|
||||
|
||||
3234
package-lock.json
generated
3234
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -19,13 +19,13 @@
|
||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.679.0",
|
||||
"@aws-sdk/client-elasticache": "^3.675.0",
|
||||
"@aws-sdk/client-s3": "^3.689.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.675.0",
|
||||
"@aws-sdk/client-ses": "^3.675.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.675.0",
|
||||
"@opensearch-project/opensearch": "^2.12.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.693.0",
|
||||
"@aws-sdk/client-elasticache": "^3.693.0",
|
||||
"@aws-sdk/client-s3": "^3.693.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.693.0",
|
||||
"@aws-sdk/client-ses": "^3.693.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.693.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"aws4": "^1.13.2",
|
||||
@@ -34,20 +34,20 @@
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"canvas": "^2.11.2",
|
||||
"chart.js": "^4.4.5",
|
||||
"chart.js": "^4.4.6",
|
||||
"cloudinary": "^2.5.1",
|
||||
"compression": "^1.7.4",
|
||||
"compression": "^1.7.5",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "2.8.5",
|
||||
"csrf": "^3.1.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-admin": "^13.0.0",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"inline-css": "^4.0.2",
|
||||
"intuit-oauth": "^4.1.2",
|
||||
"intuit-oauth": "^4.1.3",
|
||||
"ioredis": "^5.4.1",
|
||||
"json-2-csv": "^5.5.6",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -56,18 +56,18 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-mailjet": "^6.0.6",
|
||||
"node-persist": "^4.0.3",
|
||||
"nodemailer": "^6.9.15",
|
||||
"phone": "^3.1.51",
|
||||
"nodemailer": "^6.9.16",
|
||||
"phone": "^3.1.53",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"redis": "^4.7.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"soap": "^1.1.5",
|
||||
"socket.io": "^4.8.0",
|
||||
"soap": "^1.1.6",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^10.0.3",
|
||||
"twilio": "^4.23.0",
|
||||
"uuid": "^10.0.0",
|
||||
"winston": "^3.15.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
|
||||
@@ -87,6 +87,21 @@ mutation RECEIVE_MESSAGE($msg: [messages_insert_input!]!) {
|
||||
updated_at
|
||||
unreadcnt
|
||||
phone_num
|
||||
label
|
||||
job_conversations {
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
}
|
||||
}
|
||||
messages_aggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
conversationid
|
||||
created_at
|
||||
@@ -2544,3 +2559,69 @@ exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports.GET_CONVERSATIONS = `query GET_CONVERSATIONS($bodyshopId: uuid!) {
|
||||
conversations(
|
||||
where: { bodyshopid: { _eq: $bodyshopId }, archived: { _eq: false } },
|
||||
order_by: { updated_at: desc },
|
||||
limit: 50
|
||||
) {
|
||||
phone_num
|
||||
id
|
||||
updated_at
|
||||
unreadcnt
|
||||
archived
|
||||
label
|
||||
messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
job_conversations {
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports.MARK_MESSAGES_AS_READ = `mutation MARK_MESSAGES_AS_READ($conversationId: uuid!) {
|
||||
update_messages(where: { conversationid: { _eq: $conversationId } }, _set: { read: true }) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports.GET_CONVERSATION_DETAILS = `
|
||||
query GET_CONVERSATION_DETAILS($conversationId: uuid!) {
|
||||
conversation: conversations_by_pk(id: $conversationId) {
|
||||
id
|
||||
phone_num
|
||||
updated_at
|
||||
label
|
||||
job_conversations {
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
}
|
||||
}
|
||||
}
|
||||
messages: messages(where: { conversationid: { _eq: $conversationId } }, order_by: { created_at: asc }) {
|
||||
id
|
||||
text
|
||||
created_at
|
||||
read
|
||||
isoutbound
|
||||
userid
|
||||
image_path
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -11,7 +11,11 @@ const logger = require("../utils/logger");
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
|
||||
exports.receive = async (req, res) => {
|
||||
//Perform request validation
|
||||
// Perform request validation
|
||||
const {
|
||||
ioRedis,
|
||||
ioHelpers: { getBodyshopRoom }
|
||||
} = req;
|
||||
|
||||
logger.log("sms-inbound", "DEBUG", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
@@ -20,7 +24,7 @@ exports.receive = async (req, res) => {
|
||||
image_path: generateMediaArray(req.body)
|
||||
});
|
||||
|
||||
if (!!!req.body || !!!req.body.MessagingServiceSid || !!!req.body.SmsMessageSid) {
|
||||
if (!req.body || !req.body.MessagingServiceSid || !req.body.SmsMessageSid) {
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
@@ -28,170 +32,169 @@ exports.receive = async (req, res) => {
|
||||
image_path: generateMediaArray(req.body),
|
||||
type: "malformed-request"
|
||||
});
|
||||
res.status(400);
|
||||
res.json({ success: false, error: "Malformed Request" });
|
||||
} else {
|
||||
try {
|
||||
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
||||
mssid: req.body.MessagingServiceSid,
|
||||
phone: phone(req.body.From).phoneNumber
|
||||
});
|
||||
res.status(400).json({ success: false, error: "Malformed Request" });
|
||||
return;
|
||||
}
|
||||
|
||||
let newMessage = {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body)
|
||||
};
|
||||
if (response.bodyshops[0]) {
|
||||
//Found a bodyshop - should always happen.
|
||||
if (response.bodyshops[0].conversations.length === 0) {
|
||||
//No conversation Found, create one.
|
||||
//console.log("[SMS Receive] No conversation found. Creating one.");
|
||||
newMessage.conversation = {
|
||||
data: {
|
||||
bodyshopid: response.bodyshops[0].id,
|
||||
phone_num: phone(req.body.From).phoneNumber
|
||||
}
|
||||
};
|
||||
} else if (response.bodyshops[0].conversations.length === 1) {
|
||||
//Just add it to the conversation
|
||||
//console.log("[SMS Receive] Conversation found. Added ID.");
|
||||
newMessage.conversationid = response.bodyshops[0].conversations[0].id;
|
||||
} else {
|
||||
//We should never get here.
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
type: "duplicate-phone"
|
||||
});
|
||||
}
|
||||
try {
|
||||
let insertresp;
|
||||
if (response.bodyshops[0].conversations[0]) {
|
||||
insertresp = await client.request(queries.INSERT_MESSAGE, {
|
||||
msg: newMessage,
|
||||
conversationid: response.bodyshops[0].conversations[0] && response.bodyshops[0].conversations[0].id
|
||||
});
|
||||
} else {
|
||||
insertresp = await client.request(queries.RECEIVE_MESSAGE, {
|
||||
msg: newMessage
|
||||
});
|
||||
try {
|
||||
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
||||
mssid: req.body.MessagingServiceSid,
|
||||
phone: phone(req.body.From).phoneNumber
|
||||
});
|
||||
|
||||
let newMessage = {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body)
|
||||
};
|
||||
|
||||
if (response.bodyshops[0]) {
|
||||
if (response.bodyshops[0].conversations.length === 0) {
|
||||
// No conversation found, create one
|
||||
newMessage.conversation = {
|
||||
data: {
|
||||
bodyshopid: response.bodyshops[0].id,
|
||||
phone_num: phone(req.body.From).phoneNumber
|
||||
}
|
||||
const message = insertresp.insert_messages.returning[0];
|
||||
const data = {
|
||||
type: "messaging-inbound",
|
||||
conversationid: message.conversationid || "",
|
||||
text: message.text || "",
|
||||
messageid: message.id || "",
|
||||
phone_num: message.conversation.phone_num || ""
|
||||
};
|
||||
|
||||
try {
|
||||
// Insert new conversation and message
|
||||
const insertresp = await client.request(queries.RECEIVE_MESSAGE, { msg: newMessage });
|
||||
|
||||
// Safely access conversation and message
|
||||
const createdConversation = insertresp?.insert_messages?.returning?.[0]?.conversation || null;
|
||||
const message = insertresp?.insert_messages?.returning?.[0] || {
|
||||
text: "",
|
||||
image: false,
|
||||
image_path: []
|
||||
};
|
||||
|
||||
const fcmresp = await admin.messaging().send({
|
||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
||||
notification: {
|
||||
title: InstanceManager({
|
||||
imex: `ImEX Online Message - ${data.phone_num}`,
|
||||
rome: `Rome Online Message - ${data.phone_num}`,
|
||||
promanager: `ProManager Message - ${data.phone_num}`
|
||||
}),
|
||||
body: message.image_path ? `Image ${message.text}` : message.text
|
||||
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //TODO:AIO Resolve addresses for other instances
|
||||
if (!createdConversation) {
|
||||
throw new Error("Conversation data is missing from the response.");
|
||||
}
|
||||
|
||||
ioRedis.to(getBodyshopRoom(response.bodyshops[0].id)).emit("new-conversation", {
|
||||
conversation: {
|
||||
...createdConversation,
|
||||
messages_aggregate: {
|
||||
aggregate: {
|
||||
count: 1 // Adjust dynamically based on your logic or default to 0
|
||||
}
|
||||
}
|
||||
},
|
||||
data
|
||||
message
|
||||
});
|
||||
|
||||
logger.log("sms-inbound-success", "DEBUG", "api", null, {
|
||||
newMessage,
|
||||
fcmresp
|
||||
createdConversation
|
||||
});
|
||||
|
||||
res.status(200).send("");
|
||||
} catch (e2) {
|
||||
return;
|
||||
} catch (e) {
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
error: e2
|
||||
error: e
|
||||
});
|
||||
|
||||
res.sendStatus(500).json(e2);
|
||||
res.status(500).json(e);
|
||||
return;
|
||||
}
|
||||
} else if (response.bodyshops[0].conversations.length === 1) {
|
||||
// Add to the existing conversation
|
||||
// conversation UPDATED
|
||||
newMessage.conversationid = response.bodyshops[0].conversations[0].id;
|
||||
} else {
|
||||
// Duplicate phone error
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
type: "duplicate-phone"
|
||||
});
|
||||
res.status(400).json({ success: false, error: "Duplicate phone number" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Insert message into an existing conversation
|
||||
const insertresp = await client.request(queries.INSERT_MESSAGE, {
|
||||
msg: newMessage,
|
||||
conversationid: newMessage.conversationid
|
||||
});
|
||||
|
||||
const message = insertresp.insert_messages.returning[0];
|
||||
const data = {
|
||||
type: "messaging-inbound",
|
||||
conversationid: message.conversationid || "",
|
||||
text: message.text || "",
|
||||
messageid: message.id || "",
|
||||
phone_num: message.conversation.phone_num || ""
|
||||
};
|
||||
|
||||
const fcmresp = await admin.messaging().send({
|
||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
||||
notification: {
|
||||
title: InstanceManager({
|
||||
imex: `ImEX Online Message - ${data.phone_num}`,
|
||||
rome: `Rome Online Message - ${data.phone_num}`,
|
||||
promanager: `ProManager Message - ${data.phone_num}`
|
||||
}),
|
||||
body: message.image_path ? `Image ${message.text}` : message.text
|
||||
},
|
||||
data
|
||||
});
|
||||
|
||||
logger.log("sms-inbound-success", "DEBUG", "api", null, {
|
||||
newMessage,
|
||||
fcmresp
|
||||
});
|
||||
|
||||
// Broadcast new message to the conversation room
|
||||
const room = `conversation-${newMessage.conversationid}`;
|
||||
ioRedis.to(room).emit("new-message", newMessage);
|
||||
|
||||
res.status(200).send("");
|
||||
} catch (e) {
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
error: e
|
||||
});
|
||||
|
||||
res.status(500).json(e);
|
||||
}
|
||||
} catch (e1) {
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
error: e1
|
||||
});
|
||||
res.sendStatus(500).json(e1);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
error: e
|
||||
});
|
||||
res.status(500).json(e);
|
||||
}
|
||||
};
|
||||
|
||||
// const sampleMessage: {
|
||||
// "ToCountry": "CA",
|
||||
// "ToState": "BC",
|
||||
// "SmsMessageSid": "SMad7bddaf3454c0904999d6018b1e8f49",
|
||||
// "NumMedia": "0",
|
||||
// "ToCity": "Vancouver",
|
||||
// "FromZip": "",
|
||||
// "SmsSid": "SMad7bddaf3454c0904999d6018b1e8f49",
|
||||
// "FromState": "BC",
|
||||
// "SmsStatus": "received",
|
||||
// "FromCity": "VANCOUVER",
|
||||
// "Body": "Hi",
|
||||
// "FromCountry": "CA",
|
||||
// "To": "+16043301606",
|
||||
// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1",
|
||||
// "ToZip": "",
|
||||
// "NumSegments": "1",
|
||||
// "MessageSid": "SMad7bddaf3454c0904999d6018b1e8f49",
|
||||
// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c",
|
||||
// "From": "+16049992002",
|
||||
// "ApiVersion": "2010-04-01"
|
||||
// }
|
||||
// ] req.body {
|
||||
// [0] ToCountry: 'CA',
|
||||
// [0] MediaContentType0: 'image/jpeg',
|
||||
// [0] ToState: 'BC',
|
||||
// [0] SmsMessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
|
||||
// [0] NumMedia: '1',
|
||||
// [0] ToCity: 'Vancouver',
|
||||
// [0] FromZip: '',
|
||||
// [0] SmsSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
|
||||
// [0] FromState: 'BC',
|
||||
// [0] SmsStatus: 'received',
|
||||
// [0] FromCity: 'VANCOUVER',
|
||||
// [0] Body: '',
|
||||
// [0] FromCountry: 'CA',
|
||||
// [0] To: '+16043301606',
|
||||
// [0] MessagingServiceSid: 'MG6e259e2add04ffa0d0aa355038670ee1',
|
||||
// [0] ToZip: '',
|
||||
// [0] NumSegments: '1',
|
||||
// [0] MessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
|
||||
// [0] AccountSid: 'AC6c09d337d6b9c68ab6488c2052bd457c',
|
||||
// [0] From: '+16049992002',
|
||||
// [0] MediaUrl0: 'https://api.twilio.com/2010-04-01/Accounts/AC6c09d337d6b9c68ab6488c2052bd457c/Messages/MM14fa2851ba26e0dc2b62073f8e7cdf27/Media/MEf129dd37979852f395eb29ffb126e19e',
|
||||
// [0] ApiVersion: '2010-04-01'
|
||||
// [0] }
|
||||
|
||||
// [0] MediaContentType0: 'image/jpeg',
|
||||
// MediaContentType0: 'video/3gpp',
|
||||
|
||||
const generateMediaArray = (body) => {
|
||||
const { NumMedia } = body;
|
||||
if (parseInt(NumMedia) > 0) {
|
||||
//stuff
|
||||
const ret = [];
|
||||
for (var i = 0; i < parseInt(NumMedia); i++) {
|
||||
for (let i = 0; i < parseInt(NumMedia); i++) {
|
||||
ret.push(body[`MediaUrl${i}`]);
|
||||
}
|
||||
return ret;
|
||||
|
||||
@@ -14,6 +14,7 @@ const gqlClient = require("../graphql-client/graphql-client").client;
|
||||
|
||||
exports.send = (req, res) => {
|
||||
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
||||
const { ioRedis } = req;
|
||||
|
||||
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
|
||||
messagingServiceSid: messagingServiceSid,
|
||||
@@ -59,6 +60,13 @@ exports.send = (req, res) => {
|
||||
conversationid: newMessage.conversationid || ""
|
||||
};
|
||||
|
||||
// TODO Verify
|
||||
// const messageData = response.insert_messages.returning[0];
|
||||
|
||||
// Broadcast new message to conversation room
|
||||
const room = `conversation-${conversationid}`;
|
||||
ioRedis.to(room).emit("new-message", newMessage);
|
||||
|
||||
admin.messaging().send({
|
||||
topic: `${imexshopid}-messaging`,
|
||||
data
|
||||
|
||||
@@ -11,6 +11,7 @@ const { admin } = require("../firebase/firebase-handler");
|
||||
|
||||
exports.status = (req, res) => {
|
||||
const { SmsSid, SmsStatus } = req.body;
|
||||
const { ioRedis } = req;
|
||||
client
|
||||
.request(queries.UPDATE_MESSAGE_STATUS, {
|
||||
msid: SmsSid,
|
||||
@@ -21,6 +22,12 @@ exports.status = (req, res) => {
|
||||
msid: SmsSid,
|
||||
fields: { status: SmsStatus }
|
||||
});
|
||||
// TODO Verify
|
||||
const conversationId = response.update_messages.returning[0].conversationid;
|
||||
ioRedis.to(`conversation-${conversationId}`).emit("message-status-updated", {
|
||||
messageId: SmsSid,
|
||||
status: SmsStatus
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log("sms-status-update-error", "ERROR", "api", null, {
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const { admin } = require("../firebase/firebase-handler");
|
||||
const { MARK_MESSAGES_AS_READ, GET_CONVERSATIONS, GET_CONVERSATION_DETAILS } = require("../graphql-client/queries");
|
||||
const { phone } = require("phone");
|
||||
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const twilio = require("twilio");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
const twilioClient = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
||||
|
||||
const redisSocketEvents = ({
|
||||
io,
|
||||
@@ -113,6 +121,7 @@ const redisSocketEvents = ({
|
||||
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
||||
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
||||
};
|
||||
|
||||
// Disconnect Events
|
||||
const registerDisconnectEvents = (socket) => {
|
||||
const disconnect = () => {
|
||||
@@ -129,10 +138,168 @@ const redisSocketEvents = ({
|
||||
|
||||
socket.on("disconnect", disconnect);
|
||||
};
|
||||
// Messaging Events
|
||||
const registerMessagingEvents = (socket) => {
|
||||
const openMessaging = async (bodyshopUUID) => {
|
||||
try {
|
||||
const conversations = await client.request(GET_CONVERSATIONS, { bodyshopId: bodyshopUUID });
|
||||
socket.emit("messaging-list", { conversations });
|
||||
} catch (error) {
|
||||
logger.log("error", "Failed to fetch conversations", error);
|
||||
socket.emit("error", { message: "Failed to fetch conversations" });
|
||||
}
|
||||
};
|
||||
|
||||
const joinConversation = async (conversationId) => {
|
||||
try {
|
||||
const room = `conversation-${conversationId}`;
|
||||
socket.join(room);
|
||||
|
||||
// Fetch conversation details and messages
|
||||
const data = await client.request(GET_CONVERSATION_DETAILS, { conversationId });
|
||||
socket.emit("conversation-details", data); // Send data to the client
|
||||
} catch (error) {
|
||||
logger.log("error", "Failed to join conversation", error);
|
||||
socket.emit("error", { message: "Failed to join conversation" });
|
||||
}
|
||||
};
|
||||
|
||||
const markAsRead = async ({ conversationId, userId, imexshopid, bodyshopId }) => {
|
||||
try {
|
||||
await client.request(MARK_MESSAGES_AS_READ, { conversationId, userId });
|
||||
|
||||
// Fetch the updated unread count for this conversation
|
||||
const conversations = await client.request(GET_CONVERSATIONS, { bodyshopId });
|
||||
|
||||
// Emit the updated unread count to all clients
|
||||
const room = `conversation-${conversationId}`;
|
||||
io.to(room).emit("messaging-list", { conversations });
|
||||
|
||||
admin.messaging().send({
|
||||
topic: `${imexshopid}-messaging`,
|
||||
data: {
|
||||
type: "messaging-mark-conversation-read",
|
||||
conversationid: conversationId || ""
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("Failed to mark messages as read", "error", null, null, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
socket.emit("error", { message: "Failed to mark messages as read" });
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = (data) => {
|
||||
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid, user } = data;
|
||||
logger.log("sms-outbound", "DEBUG", user.email, null, {
|
||||
messagingServiceSid: messagingServiceSid,
|
||||
to: phone(to).phoneNumber,
|
||||
mediaUrl: selectedMedia.map((i) => i.src),
|
||||
text: body,
|
||||
conversationid,
|
||||
isoutbound: true,
|
||||
userid: user.email,
|
||||
image: selectedMedia?.length > 0,
|
||||
image_path: selectedMedia?.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
});
|
||||
|
||||
if (!!to && !!messagingServiceSid && (!!body || !!selectedMedia?.length > 0) && !!conversationid) {
|
||||
twilioClient.messages
|
||||
.create({
|
||||
body: body,
|
||||
messagingServiceSid: messagingServiceSid,
|
||||
to: phone(to).phoneNumber,
|
||||
mediaUrl: selectedMedia.map((i) => i.src)
|
||||
})
|
||||
.then((message) => {
|
||||
let newMessage = {
|
||||
msid: message.sid,
|
||||
text: body,
|
||||
conversationid,
|
||||
isoutbound: true,
|
||||
userid: user.email,
|
||||
image: selectedMedia?.length > 0,
|
||||
image_path: selectedMedia?.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
};
|
||||
gqlClient
|
||||
.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid })
|
||||
.then((r2) => {
|
||||
logger.log("sms-outbound-success", "DEBUG", user.email, null, {
|
||||
msid: message.sid,
|
||||
conversationid
|
||||
});
|
||||
|
||||
const data = {
|
||||
type: "messaging-outbound",
|
||||
conversationid: newMessage.conversationid || ""
|
||||
};
|
||||
|
||||
// TODO Verify
|
||||
// const messageData = response.insert_messages.returning[0];
|
||||
|
||||
// Broadcast new message to conversation room
|
||||
const room = `conversation-${conversationid}`;
|
||||
io.to(room).emit("new-message", newMessage);
|
||||
|
||||
admin.messaging().send({
|
||||
topic: `${imexshopid}-messaging`,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch((e2) => {
|
||||
logger.log("sms-outbound-error", "ERROR", user.email, null, {
|
||||
msid: message.sid,
|
||||
conversationid,
|
||||
error: e2
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((e1) => {
|
||||
logger.log("sms-outbound-error", "ERROR", user.email, null, {
|
||||
conversationid,
|
||||
error: e1
|
||||
});
|
||||
});
|
||||
} else {
|
||||
logger.log("sms-outbound-error", "ERROR", user.email, null, {
|
||||
type: "missing-parameters",
|
||||
messagingServiceSid: messagingServiceSid,
|
||||
to: phone(to).phoneNumber,
|
||||
text: body,
|
||||
conversationid,
|
||||
isoutbound: true,
|
||||
userid: user.email,
|
||||
image: selectedMedia?.length > 0,
|
||||
image_path: selectedMedia?.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const leaveConversation = (conversationId) => {
|
||||
try {
|
||||
const room = `conversation-${conversationId}`;
|
||||
socket.leave(room);
|
||||
|
||||
// Optionally notify the client
|
||||
socket.emit("conversation-left", { conversationId });
|
||||
} catch (error) {
|
||||
socket.emit("error", { message: "Failed to leave conversation" });
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("leave-conversation", leaveConversation);
|
||||
socket.on("send-message", sendMessage);
|
||||
socket.on("mark-as-read", markAsRead);
|
||||
socket.on("join-conversation", joinConversation);
|
||||
socket.on("open-messaging", openMessaging);
|
||||
};
|
||||
|
||||
// Call Handlers
|
||||
registerRoomAndBroadcastEvents(socket);
|
||||
registerUpdateEvents(socket);
|
||||
registerMessagingEvents(socket);
|
||||
registerDisconnectEvents(socket);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user