IO-3000 Adjusted first approach at messaging WS changes.
This commit is contained in:
@@ -2,20 +2,26 @@ import { useApolloClient } from "@apollo/client";
|
|||||||
import { getToken, onMessage } from "@firebase/messaging";
|
import { getToken, onMessage } from "@firebase/messaging";
|
||||||
import { Button, notification, Space } from "antd";
|
import { Button, notification, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import FcmHandler from "../../utils/fcm-handler";
|
import FcmHandler from "../../utils/fcm-handler";
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||||
|
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||||
|
|
||||||
|
//Register WS handlers
|
||||||
|
registerMessagingHandlers({ socket, client });
|
||||||
|
|
||||||
async function SubscribeToTopic() {
|
async function SubscribeToTopic() {
|
||||||
try {
|
try {
|
||||||
const r = await axios.post("/notifications/subscribe", {
|
const r = await axios.post("/notifications/subscribe", {
|
||||||
@@ -61,7 +67,11 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
|
|
||||||
SubscribeToTopic();
|
SubscribeToTopic();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [bodyshop]);
|
|
||||||
|
return () => {
|
||||||
|
unregisterMessagingHandlers({ socket });
|
||||||
|
};
|
||||||
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleMessage(payload) {
|
function handleMessage(payload) {
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
|
|
||||||
|
export function registerMessagingHandlers({ socket, client }) {
|
||||||
|
if (!(socket && client)) return;
|
||||||
|
function handleNewMessageSummary(message) {
|
||||||
|
console.log("🚀 ~ SUMMARY CONSOLE LOG:", message);
|
||||||
|
|
||||||
|
if (!message.isoutbound) {
|
||||||
|
//It's an inbound message.
|
||||||
|
if (!message.existingConversation) {
|
||||||
|
//Do a read query.
|
||||||
|
const queryResults = client.cache.readQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: {}
|
||||||
|
});
|
||||||
|
// Do a write query. Assume 0 unread messages to utilize code below.
|
||||||
|
client.cache.writeQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: {},
|
||||||
|
data: {
|
||||||
|
conversations: [
|
||||||
|
{ ...message.newConversation, messages_aggregate: { aggregate: { count: 0 } } },
|
||||||
|
...queryResults
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({
|
||||||
|
__typename: "conversations",
|
||||||
|
id: message.conversationId
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
updated_at: () => new Date(),
|
||||||
|
messages_aggregate(cached) {
|
||||||
|
return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
client.cache.modify({
|
||||||
|
fields: {
|
||||||
|
conversations(existingConversations = [], { readField }) {
|
||||||
|
return [
|
||||||
|
{ __ref: `conversations:${message.conversationId}` }, // TODO: This throws the cache merging error in apollo.
|
||||||
|
...existingConversations.filter((c) => c.__ref !== `conversations:${message.conversationId}`)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.cache.modify({
|
||||||
|
fields: {
|
||||||
|
messages_aggregate(cached) {
|
||||||
|
return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//It's an outbound message
|
||||||
|
//Update the last updated for conversations in the list. If it's new, add it in.
|
||||||
|
// If it isn't just update the last updated at.
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({
|
||||||
|
__typename: "conversations",
|
||||||
|
id: message.conversationId
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
updated_at: () => message.newMessage.updated_at
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewMessageDetailed(message) {
|
||||||
|
console.log("🚀 ~ DETAIL CONSOLE LOG:", message);
|
||||||
|
//They're looking at the conversation right now. Need to merge into the list of messages i.e. append to the end.
|
||||||
|
//Add the message to the overall cache.
|
||||||
|
|
||||||
|
//Handle outbound messages
|
||||||
|
if (message.newMessage.isoutbound) {
|
||||||
|
const queryResults = client.cache.readQuery({
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: { conversationId: message.newMessage.conversationid }
|
||||||
|
});
|
||||||
|
client.cache.writeQuery({
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: { conversationId: message.newMessage.conversationid },
|
||||||
|
data: {
|
||||||
|
...queryResults,
|
||||||
|
conversations_by_pk: {
|
||||||
|
...queryResults.conversations_by_pk,
|
||||||
|
messages: [...queryResults.conversations_by_pk.messages, message.newMessage]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// We got this as a receive.
|
||||||
|
else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageChanged(message) {
|
||||||
|
//Find it in the cache, and just update it based on what was sent.
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({
|
||||||
|
__typename: "messages",
|
||||||
|
id: message.id
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
//TODO: see if there is a way to have this update all fields e.g. only spread in updates rather than prescribing
|
||||||
|
updated_at: () => new Date(),
|
||||||
|
status(cached) {
|
||||||
|
return message.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConversationChanged(conversation) {
|
||||||
|
//If it was archived, marked unread, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
|
socket.on("new-message-detailed", handleNewMessageDetailed);
|
||||||
|
socket.on("message-changed", handleMessageChanged);
|
||||||
|
socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterMessagingHandlers({ socket }) {
|
||||||
|
if (!socket) return;
|
||||||
|
socket.off("new-message-summary");
|
||||||
|
socket.off("new-message-detailed");
|
||||||
|
socket.off("message-changed");
|
||||||
|
socket.off("message-changed");
|
||||||
|
socket.off("conversation-changed");
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
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 { createStructuredSelector } from "reselect";
|
||||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
@@ -25,12 +25,7 @@ function ChatConversationListComponent({
|
|||||||
setSelectedConversation,
|
setSelectedConversation,
|
||||||
loadMoreConversations
|
loadMoreConversations
|
||||||
}) {
|
}) {
|
||||||
const cache = new CellMeasurerCache({
|
const renderConversation = (index) => {
|
||||||
fixedWidth: true,
|
|
||||||
defaultHeight: 60
|
|
||||||
});
|
|
||||||
|
|
||||||
const rowRenderer = ({ index, key, style, parent }) => {
|
|
||||||
const item = conversationList[index];
|
const item = conversationList[index];
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
@@ -52,7 +47,8 @@ function ChatConversationListComponent({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
|
||||||
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
@@ -60,40 +56,27 @@ function ChatConversationListComponent({
|
|||||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
|
<List.Item
|
||||||
<List.Item
|
key={item.id}
|
||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
style={style}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
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>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
</Card>
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
</List.Item>
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ height, width }) => (
|
data={conversationList}
|
||||||
<VirtualizedList
|
itemContent={(index) => renderConversation(index)}
|
||||||
height={height}
|
style={{ height: "100%", width: "100%" }}
|
||||||
width={width}
|
endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom
|
||||||
rowCount={conversationList.length}
|
/>
|
||||||
rowHeight={cache.rowHeight}
|
|
||||||
rowRenderer={rowRenderer}
|
|
||||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
|
||||||
if (scrollTop + clientHeight === scrollHeight) {
|
|
||||||
loadMoreConversations();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.chat-list-container {
|
.chat-list-container {
|
||||||
overflow: hidden;
|
height: 100%; /* Ensure it takes up the full available height */
|
||||||
height: 100%;
|
|
||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
|
overflow: auto; /* Allow scrolling for the Virtuoso component */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
@@ -14,3 +14,24 @@
|
|||||||
color: #ff7a00;
|
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,13 +1,14 @@
|
|||||||
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import React, { useState } from "react";
|
import axios from "axios";
|
||||||
|
import React, { useEffect, useState, useContext } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||||
|
import { GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
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 { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
@@ -27,41 +28,34 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
const { socket } = useContext(SocketContext);
|
||||||
variables: { conversationId: selectedConversation }
|
|
||||||
});
|
useEffect(() => {
|
||||||
|
socket.emit("join-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.emit("leave-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation });
|
||||||
|
};
|
||||||
|
}, [selectedConversation, bodyshop, socket]);
|
||||||
|
|
||||||
|
// const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||||||
|
// variables: { conversationId: selectedConversation }
|
||||||
|
// });
|
||||||
|
|
||||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
|
|
||||||
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 } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const unreadCount =
|
const unreadCount =
|
||||||
data &&
|
convoData &&
|
||||||
data.messages &&
|
convoData.conversations_by_pk &&
|
||||||
data.messages.reduce((acc, val) => {
|
convoData.conversations_by_pk.messages &&
|
||||||
|
convoData.conversations_by_pk.messages.reduce((acc, val) => {
|
||||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const handleMarkConversationAsRead = async () => {
|
const handleMarkConversationAsRead = async () => {
|
||||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||||
setMarkingAsReadInProgress(true);
|
setMarkingAsReadInProgress(true);
|
||||||
await markConversationRead({});
|
// await markConversationRead({});
|
||||||
await axios.post("/sms/markConversationRead", {
|
await axios.post("/sms/markConversationRead", {
|
||||||
conversationid: selectedConversation,
|
conversationid: selectedConversation,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid
|
||||||
@@ -72,9 +66,9 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatConversationComponent
|
<ChatConversationComponent
|
||||||
subState={[loading || convoLoading, error || convoError]}
|
subState={[convoLoading, convoError]}
|
||||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||||
messages={data ? data.messages : []}
|
messages={convoData ? convoData.conversations_by_pk.messages : []}
|
||||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,105 +2,90 @@ import Icon from "@ant-design/icons";
|
|||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import dayjs from "../../utils/day";
|
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 { 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 { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
|
|
||||||
export default function ChatMessageListComponent({ messages }) {
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
const virtualizedListRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
const _cache = new CellMeasurerCache({
|
// Scroll to the bottom after a short delay when the component mounts
|
||||||
fixedWidth: true,
|
useEffect(() => {
|
||||||
// minHeight: 50,
|
const timer = setTimeout(() => {
|
||||||
defaultHeight: 100
|
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) => {
|
// Scroll to the bottom after the new messages are rendered
|
||||||
//console.log("Scrolling to", messages.length);
|
useEffect(() => {
|
||||||
// !!virtualizedListRef.current &&
|
if (virtuosoRef.current) {
|
||||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
// Allow the DOM and Virtuoso to fully render the new data
|
||||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
setTimeout(() => {
|
||||||
//Scrolling does not work on this version of React.
|
virtuosoRef.current.scrollToIndex({
|
||||||
};
|
index: messages.length - 1,
|
||||||
|
align: "end", // Ensure the last message is fully visible
|
||||||
useEffect(scrollToBottom, [messages]);
|
behavior: "smooth" // Smooth scrolling
|
||||||
|
});
|
||||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
}, 50); // Slight delay to ensure layout recalculates
|
||||||
|
}
|
||||||
|
}, [messages]); // Triggered when new messages are added
|
||||||
|
//TODO: Does this one need to come into the render of the method?
|
||||||
|
const renderMessage = (index) => {
|
||||||
|
const message = messages[index];
|
||||||
return (
|
return (
|
||||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
||||||
{({ measure, registerChild }) => (
|
<div className="message msgmargin">
|
||||||
<div
|
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||||
ref={registerChild}
|
<div>
|
||||||
onLoad={measure}
|
{message.image_path &&
|
||||||
style={style}
|
message.image_path.map((i, idx) => (
|
||||||
className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
|
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||||
>
|
<a href={i} target="__blank" rel="noopener noreferrer">
|
||||||
<div className="message msgmargin">
|
<img alt="Received" className="message-img" src={i} />
|
||||||
{MessageRender(messages[index])}
|
</a>
|
||||||
{StatusRender(messages[index].status)}
|
</div>
|
||||||
|
))}
|
||||||
|
<div>{message.text}</div>
|
||||||
</div>
|
</div>
|
||||||
{messages[index].isoutbound && (
|
</Tooltip>
|
||||||
<div style={{ fontSize: 10 }}>
|
{message.status && (
|
||||||
{i18n.t("messaging.labels.sentby", {
|
<div className="message-status">
|
||||||
by: messages[index].userid,
|
<Icon
|
||||||
time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
|
component={message.status === "sent" ? MdDone : message.status === "delivered" ? MdDoneAll : null}
|
||||||
})}
|
className="message-icon"
|
||||||
</div>
|
/>
|
||||||
)}
|
</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>
|
||||||
)}
|
)}
|
||||||
</CellMeasurer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ height, width }) => (
|
ref={virtuosoRef}
|
||||||
<List
|
data={messages}
|
||||||
ref={virtualizedListRef}
|
itemContent={(index) => renderMessage(index)}
|
||||||
width={width}
|
followOutput="smooth" // Ensure smooth scrolling when new data is appended
|
||||||
height={height}
|
style={{ height: "100%", width: "100%" }}
|
||||||
rowHeight={_cache.rowHeight}
|
/>
|
||||||
rowRenderer={_rowRenderer}
|
|
||||||
rowCount={messages.length}
|
|
||||||
overscanRowCount={10}
|
|
||||||
estimatedRowSize={150}
|
|
||||||
scrollToIndex={messages.length}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</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 {
|
.message-icon {
|
||||||
//position: absolute;
|
|
||||||
// bottom: 0rem;
|
|
||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
border: #000000;
|
border: #000000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: 0 0.1rem;
|
margin: 0 0.1rem;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
right: 0.3rem;
|
right: 0.3rem;
|
||||||
|
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
//width: 300px;
|
|
||||||
//border: solid 1px #eee;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0.8rem 0rem;
|
margin: 0.8rem 0rem;
|
||||||
|
overflow: hidden; // Ensure the content scrolls correctly
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
//margin-top: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0.5rem; // Add padding to avoid edge clipping
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 0.25rem 0.8rem;
|
padding: 0.25rem 0.8rem;
|
||||||
//margin-top: 5px;
|
|
||||||
// margin-bottom: 5px;
|
|
||||||
//display: inline-block;
|
|
||||||
|
|
||||||
.message-img {
|
.message-img {
|
||||||
max-width: 10rem;
|
max-width: 10rem;
|
||||||
@@ -56,7 +49,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message.last:before {
|
.yours .message:last-child:before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
@@ -68,7 +61,7 @@
|
|||||||
border-bottom-right-radius: 15px;
|
border-bottom-right-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message.last:after {
|
.yours .message:last-child:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -88,12 +81,11 @@
|
|||||||
color: white;
|
color: white;
|
||||||
margin-left: 25%;
|
margin-left: 25%;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||||
background-attachment: fixed;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 0.6rem;
|
padding-bottom: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine .message.last:before {
|
.mine .message:last-child:before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
@@ -102,11 +94,10 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||||
background-attachment: fixed;
|
|
||||||
border-bottom-left-radius: 15px;
|
border-bottom-left-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine .message.last:after {
|
.mine .message:last-child:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||||
|
|
||||||
|
//TODO: Change to be a fallback incase sockets shit the bed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fcmToken) {
|
if (fcmToken) {
|
||||||
setpollInterval(0);
|
setpollInterval(0);
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ 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 unsubscribe = auth.onIdTokenChanged(async (user) => {
|
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||||
@@ -38,6 +40,8 @@ const useSocket = (bodyshop) => {
|
|||||||
case "alert-update":
|
case "alert-update":
|
||||||
store.dispatch(addAlerts(message.payload));
|
store.dispatch(addAlerts(message.payload));
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!import.meta.env.DEV) return;
|
if (!import.meta.env.DEV) return;
|
||||||
@@ -45,14 +49,12 @@ const useSocket = (bodyshop) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConnect = () => {
|
const handleConnect = () => {
|
||||||
console.log("Socket connected:", socketInstance.id);
|
|
||||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||||
setClientId(socketInstance.id);
|
setClientId(socketInstance.id);
|
||||||
store.dispatch(setWssStatus("connected"));
|
store.dispatch(setWssStatus("connected"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReconnect = (attempt) => {
|
const handleReconnect = (attempt) => {
|
||||||
console.log(`Socket reconnected after ${attempt} attempts`);
|
|
||||||
store.dispatch(setWssStatus("connected"));
|
store.dispatch(setWssStatus("connected"));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,10 +64,10 @@ const useSocket = (bodyshop) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
const handleDisconnect = () => {
|
||||||
console.log("Socket disconnected");
|
|
||||||
store.dispatch(setWssStatus("disconnected"));
|
store.dispatch(setWssStatus("disconnected"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//TODO: Check these handlers.
|
||||||
socketInstance.on("connect", handleConnect);
|
socketInstance.on("connect", handleConnect);
|
||||||
socketInstance.on("reconnect", handleReconnect);
|
socketInstance.on("reconnect", handleReconnect);
|
||||||
socketInstance.on("connect_error", handleConnectionError);
|
socketInstance.on("connect_error", handleConnectionError);
|
||||||
@@ -89,7 +91,7 @@ const useSocket = (bodyshop) => {
|
|||||||
socketRef.current = null;
|
socketRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [bodyshop]);
|
}, [bodyshop, dispatch]);
|
||||||
|
|
||||||
return { socket: socketRef.current, clientId };
|
return { socket: socketRef.current, clientId };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ export const GET_CONVERSATION_DETAILS = gql`
|
|||||||
ro_number
|
ro_number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
messages(order_by: { created_at: asc_nulls_first }) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
text
|
||||||
|
isoutbound
|
||||||
|
image
|
||||||
|
image_path
|
||||||
|
userid
|
||||||
|
created_at
|
||||||
|
read
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -114,3 +125,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();
|
fetchAlerts();
|
||||||
}, []);
|
}, [setAlerts]);
|
||||||
|
|
||||||
// Use useEffect to watch for new alerts
|
// Use useEffect to watch for new alerts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -647,7 +647,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
{true && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
||||||
<Layout style={{ minHeight: "100vh" }} className="layout-container">
|
<Layout style={{ minHeight: "100vh" }} className="layout-container">
|
||||||
<UpdateAlert />
|
<UpdateAlert />
|
||||||
<HeaderContainer />
|
<HeaderContainer />
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
export default async function FcmHandler({ client, payload }) {
|
export default async function FcmHandler({ client, payload }) {
|
||||||
console.log("FCM", payload);
|
console.log("FCM", payload);
|
||||||
switch (payload.type) {
|
// switch (payload.type) {
|
||||||
case "messaging-inbound":
|
// case "messaging-inbound":
|
||||||
client.cache.modify({
|
// client.cache.modify({
|
||||||
id: client.cache.identify({
|
// id: client.cache.identify({
|
||||||
__typename: "conversations",
|
// __typename: "conversations",
|
||||||
id: payload.conversationid
|
// id: payload.conversationid
|
||||||
}),
|
// }),
|
||||||
fields: {
|
// fields: {
|
||||||
messages_aggregate(cached) {
|
// messages_aggregate(cached) {
|
||||||
return { aggregate: { count: cached.aggregate.count + 1 } };
|
// return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
client.cache.modify({
|
// client.cache.modify({
|
||||||
fields: {
|
// fields: {
|
||||||
messages_aggregate(cached) {
|
// messages_aggregate(cached) {
|
||||||
return { aggregate: { count: cached.aggregate.count + 1 } };
|
// return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
break;
|
// break;
|
||||||
case "messaging-outbound":
|
// case "messaging-outbound":
|
||||||
client.cache.modify({
|
// client.cache.modify({
|
||||||
id: client.cache.identify({
|
// id: client.cache.identify({
|
||||||
__typename: "conversations",
|
// __typename: "conversations",
|
||||||
id: payload.conversationid
|
// id: payload.conversationid
|
||||||
}),
|
// }),
|
||||||
fields: {
|
// fields: {
|
||||||
updated_at(oldupdated0) {
|
// updated_at(oldupdated0) {
|
||||||
return new Date();
|
// return new Date();
|
||||||
}
|
// }
|
||||||
// messages_aggregate(cached) {
|
// // messages_aggregate(cached) {
|
||||||
// return { aggregate: { count: cached.aggregate.count + 1 } };
|
// // return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||||
// },
|
// // },
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
break;
|
// break;
|
||||||
case "messaging-mark-conversation-read":
|
// case "messaging-mark-conversation-read":
|
||||||
let previousUnreadCount = 0;
|
// let previousUnreadCount = 0;
|
||||||
client.cache.modify({
|
// client.cache.modify({
|
||||||
id: client.cache.identify({
|
// id: client.cache.identify({
|
||||||
__typename: "conversations",
|
// __typename: "conversations",
|
||||||
id: payload.conversationid
|
// id: payload.conversationid
|
||||||
}),
|
// }),
|
||||||
fields: {
|
// fields: {
|
||||||
messages_aggregate(cached) {
|
// messages_aggregate(cached) {
|
||||||
previousUnreadCount = cached.aggregate.count;
|
// previousUnreadCount = cached.aggregate.count;
|
||||||
return { aggregate: { count: 0 } };
|
// return { aggregate: { count: 0 } };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
client.cache.modify({
|
// client.cache.modify({
|
||||||
fields: {
|
// fields: {
|
||||||
messages_aggregate(cached) {
|
// messages_aggregate(cached) {
|
||||||
return {
|
// return {
|
||||||
aggregate: {
|
// aggregate: {
|
||||||
count: cached.aggregate.count - previousUnreadCount
|
// count: cached.aggregate.count - previousUnreadCount
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
break;
|
// break;
|
||||||
default:
|
// default:
|
||||||
console.log("No payload type set.");
|
// console.log("No payload type set.");
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { promises as fsPromises } from "fs";
|
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 { createLogger, defineConfig } from "vite";
|
||||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||||
import eslint from "vite-plugin-eslint";
|
import eslint from "vite-plugin-eslint";
|
||||||
@@ -18,28 +15,6 @@ process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
|||||||
const getFormattedTimestamp = () =>
|
const getFormattedTimestamp = () =>
|
||||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
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", {
|
export const logger = createLogger("info", {
|
||||||
allowClearScreen: false
|
allowClearScreen: false
|
||||||
});
|
});
|
||||||
@@ -108,7 +83,6 @@ export default defineConfig({
|
|||||||
gcm_sender_id: "103953800507"
|
gcm_sender_id: "103953800507"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
reactVirtualizedFix(),
|
|
||||||
react(),
|
react(),
|
||||||
eslint()
|
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}\""
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.679.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.693.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.675.0",
|
"@aws-sdk/client-elasticache": "^3.693.0",
|
||||||
"@aws-sdk/client-s3": "^3.689.0",
|
"@aws-sdk/client-s3": "^3.693.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.675.0",
|
"@aws-sdk/client-secrets-manager": "^3.693.0",
|
||||||
"@aws-sdk/client-ses": "^3.675.0",
|
"@aws-sdk/client-ses": "^3.693.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.675.0",
|
"@aws-sdk/credential-provider-node": "^3.693.0",
|
||||||
"@opensearch-project/opensearch": "^2.12.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
@@ -34,20 +34,20 @@
|
|||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"chart.js": "^4.4.5",
|
"chart.js": "^4.4.6",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.5.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.5",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^12.6.0",
|
"firebase-admin": "^13.0.0",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.2",
|
"inline-css": "^4.0.2",
|
||||||
"intuit-oauth": "^4.1.2",
|
"intuit-oauth": "^4.1.3",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"json-2-csv": "^5.5.6",
|
"json-2-csv": "^5.5.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -56,18 +56,18 @@
|
|||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.6",
|
"node-mailjet": "^6.0.6",
|
||||||
"node-persist": "^4.0.3",
|
"node-persist": "^4.0.3",
|
||||||
"nodemailer": "^6.9.15",
|
"nodemailer": "^6.9.16",
|
||||||
"phone": "^3.1.51",
|
"phone": "^3.1.53",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"soap": "^1.1.5",
|
"soap": "^1.1.6",
|
||||||
"socket.io": "^4.8.0",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^10.0.3",
|
"ssh2-sftp-client": "^10.0.3",
|
||||||
"twilio": "^4.23.0",
|
"twilio": "^4.23.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"winston": "^3.15.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ mutation RECEIVE_MESSAGE($msg: [messages_insert_input!]!) {
|
|||||||
updated_at
|
updated_at
|
||||||
unreadcnt
|
unreadcnt
|
||||||
phone_num
|
phone_num
|
||||||
|
label
|
||||||
|
job_conversations {
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages_aggregate (where: { read: { _eq: false }, isoutbound: { _eq: false } }){
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conversationid
|
conversationid
|
||||||
created_at
|
created_at
|
||||||
@@ -116,6 +131,7 @@ mutation INSERT_MESSAGE($msg: [messages_insert_input!]!, $conversationid: uuid!)
|
|||||||
id
|
id
|
||||||
archived
|
archived
|
||||||
bodyshop {
|
bodyshop {
|
||||||
|
id
|
||||||
imexshopid
|
imexshopid
|
||||||
}
|
}
|
||||||
created_at
|
created_at
|
||||||
@@ -144,6 +160,11 @@ mutation UPDATE_MESSAGE($msid: String!, $fields: messages_set_input!) {
|
|||||||
update_messages(where: { msid: { _eq: $msid } }, _set: $fields) {
|
update_messages(where: { msid: { _eq: $msid } }, _set: $fields) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
|
status
|
||||||
|
conversationid
|
||||||
|
conversation{
|
||||||
|
bodyshopid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
@@ -2544,3 +2565,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl
|
|||||||
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
||||||
const { canvastest } = require("../render/canvas-handler");
|
const { canvastest } = require("../render/canvas-handler");
|
||||||
const { alertCheck } = require("../alerts/alertcheck");
|
const { alertCheck } = require("../alerts/alertcheck");
|
||||||
|
const uuid = require("uuid").v4;
|
||||||
|
|
||||||
//Test route to ensure Express is responding.
|
//Test route to ensure Express is responding.
|
||||||
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
||||||
@@ -57,6 +58,59 @@ router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => {
|
|||||||
|
|
||||||
return res.status(500).send("Logs tested.");
|
return res.status(500).send("Logs tested.");
|
||||||
});
|
});
|
||||||
|
router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
||||||
|
const { ioRedis } = req;
|
||||||
|
ioRedis.to(`bodyshop-broadcast-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582`).emit("new-message-summary", {
|
||||||
|
isoutbound: true,
|
||||||
|
conversationId: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
msid: "SM5d053957bc0da29399b768c23bffcc0f",
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Do we need to add more content here?
|
||||||
|
ioRedis
|
||||||
|
.to(`bodyshop-conversation-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582:2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6`)
|
||||||
|
.emit("new-message-detailed", {
|
||||||
|
//
|
||||||
|
// msid: "SMbbd7703a898fef7f2c07c148ade8a6cd",
|
||||||
|
// text: "test2",
|
||||||
|
// conversationid: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
// isoutbound: true,
|
||||||
|
// userid: "patrick@imex.dev",
|
||||||
|
// image: false,
|
||||||
|
// image_path: [],
|
||||||
|
newMessage: {
|
||||||
|
conversation: {
|
||||||
|
id: uuid(),
|
||||||
|
archived: false,
|
||||||
|
bodyshop: {
|
||||||
|
id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582",
|
||||||
|
imexshopid: "APPLE"
|
||||||
|
},
|
||||||
|
created_at: "2024-11-19T19:46:38.984633+00:00",
|
||||||
|
updated_at: "2024-11-19T22:40:48.346875+00:00",
|
||||||
|
unreadcnt: 0,
|
||||||
|
phone_num: "+16138676684"
|
||||||
|
},
|
||||||
|
conversationid: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
created_at: "2024-11-19T22:40:48.346875+00:00",
|
||||||
|
id: "68604ea9-c411-43ec-ab83-899868e58819",
|
||||||
|
image_path: [],
|
||||||
|
image: false,
|
||||||
|
isoutbound: true,
|
||||||
|
msid: "SMbbd7703a898fef7f2c07c148ade8a6cd",
|
||||||
|
read: false,
|
||||||
|
text: `This is a test ${Math.round(Math.random() * 100)}`,
|
||||||
|
updated_at: "2024-11-19T22:40:48.346875+00:00",
|
||||||
|
status: "posted",
|
||||||
|
userid: "patrick@imex.dev"
|
||||||
|
},
|
||||||
|
conversationId: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
summary: false
|
||||||
|
}); // TODO: Do we need to add more content here?
|
||||||
|
|
||||||
|
return res.status(500).send("Logs tested.");
|
||||||
|
});
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
|
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ const logger = require("../utils/logger");
|
|||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
exports.receive = async (req, res) => {
|
exports.receive = async (req, res) => {
|
||||||
//Perform request validation
|
// Perform request validation
|
||||||
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
|
} = req;
|
||||||
|
|
||||||
logger.log("sms-inbound", "DEBUG", "api", null, {
|
logger.log("sms-inbound", "DEBUG", "api", null, {
|
||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
@@ -20,7 +24,7 @@ exports.receive = async (req, res) => {
|
|||||||
image_path: generateMediaArray(req.body)
|
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, {
|
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
@@ -28,170 +32,195 @@ exports.receive = async (req, res) => {
|
|||||||
image_path: generateMediaArray(req.body),
|
image_path: generateMediaArray(req.body),
|
||||||
type: "malformed-request"
|
type: "malformed-request"
|
||||||
});
|
});
|
||||||
res.status(400);
|
res.status(400).json({ success: false, error: "Malformed Request" });
|
||||||
res.json({ success: false, error: "Malformed Request" });
|
return;
|
||||||
} else {
|
}
|
||||||
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 = {
|
try {
|
||||||
msid: req.body.SmsMessageSid,
|
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
||||||
text: req.body.Body,
|
mssid: req.body.MessagingServiceSid,
|
||||||
image: !!req.body.MediaUrl0,
|
phone: phone(req.body.From).phoneNumber
|
||||||
image_path: generateMediaArray(req.body)
|
});
|
||||||
};
|
|
||||||
if (response.bodyshops[0]) {
|
let newMessage = {
|
||||||
//Found a bodyshop - should always happen.
|
msid: req.body.SmsMessageSid,
|
||||||
if (response.bodyshops[0].conversations.length === 0) {
|
text: req.body.Body,
|
||||||
//No conversation Found, create one.
|
image: !!req.body.MediaUrl0,
|
||||||
//console.log("[SMS Receive] No conversation found. Creating one.");
|
image_path: generateMediaArray(req.body)
|
||||||
newMessage.conversation = {
|
};
|
||||||
data: {
|
|
||||||
bodyshopid: response.bodyshops[0].id,
|
if (response.bodyshops[0]) {
|
||||||
phone_num: phone(req.body.From).phoneNumber
|
if (response.bodyshops[0].conversations.length === 0) {
|
||||||
}
|
// No conversation found, create one
|
||||||
};
|
newMessage.conversation = {
|
||||||
} else if (response.bodyshops[0].conversations.length === 1) {
|
data: {
|
||||||
//Just add it to the conversation
|
bodyshopid: response.bodyshops[0].id,
|
||||||
//console.log("[SMS Receive] Conversation found. Added ID.");
|
phone_num: phone(req.body.From).phoneNumber
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
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({
|
try {
|
||||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
// Insert new conversation and message
|
||||||
notification: {
|
const insertresp = await client.request(queries.RECEIVE_MESSAGE, { msg: newMessage });
|
||||||
title: InstanceManager({
|
|
||||||
imex: `ImEX Online Message - ${data.phone_num}`,
|
// Safely access conversation and message
|
||||||
rome: `Rome Online Message - ${data.phone_num}`,
|
const createdConversation = insertresp?.insert_messages?.returning?.[0]?.conversation || null;
|
||||||
promanager: `ProManager Message - ${data.phone_num}`
|
const message = insertresp?.insert_messages?.returning?.[0];
|
||||||
}),
|
|
||||||
body: message.image_path ? `Image ${message.text}` : message.text
|
if (!createdConversation) {
|
||||||
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //TODO:AIO Resolve addresses for other instances
|
throw new Error("Conversation data is missing from the response.");
|
||||||
},
|
}
|
||||||
data
|
|
||||||
|
const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id);
|
||||||
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: message.conversation.bodyshop.id,
|
||||||
|
conversationId: message.conversation.id
|
||||||
|
});
|
||||||
|
// Broadcast new message to the conversation room
|
||||||
|
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||||
|
isoutbound: false,
|
||||||
|
existingConversation: false,
|
||||||
|
newConversation: createdConversation,
|
||||||
|
conversationId: createdConversation.id,
|
||||||
|
updated_at: message.updated_at,
|
||||||
|
msid: message.sid,
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
ioRedis.to(conversationRoom).emit("new-message-detailed", {
|
||||||
|
newMessage: message,
|
||||||
|
isoutbound: false,
|
||||||
|
newConversation: createdConversation,
|
||||||
|
existingConversation: false,
|
||||||
|
conversationId: createdConversation.id,
|
||||||
|
summary: false
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log("sms-inbound-success", "DEBUG", "api", null, {
|
logger.log("sms-inbound-success", "DEBUG", "api", null, {
|
||||||
newMessage,
|
newMessage,
|
||||||
fcmresp
|
createdConversation
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).send("");
|
res.status(200).send("");
|
||||||
} catch (e2) {
|
return;
|
||||||
|
} catch (e) {
|
||||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body),
|
image_path: generateMediaArray(req.body),
|
||||||
messagingServiceSid: req.body.MessagingServiceSid,
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id);
|
||||||
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: message.conversation.bodyshop.id,
|
||||||
|
conversationId: message.conversation.id
|
||||||
|
});
|
||||||
|
// Broadcast new message to the conversation room
|
||||||
|
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||||
|
isoutbound: false,
|
||||||
|
existingConversation: true,
|
||||||
|
conversationId: conversationid,
|
||||||
|
updated_at: message.updated_at,
|
||||||
|
msid: message.sid,
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
ioRedis.to(conversationRoom).emit("new-message-detailed", {
|
||||||
|
newMessage: message,
|
||||||
|
isoutbound: false,
|
||||||
|
existingConversation: true,
|
||||||
|
conversationId: conversationid,
|
||||||
|
summary: false
|
||||||
|
});
|
||||||
|
|
||||||
|
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 generateMediaArray = (body) => {
|
||||||
const { NumMedia } = body;
|
const { NumMedia } = body;
|
||||||
if (parseInt(NumMedia) > 0) {
|
if (parseInt(NumMedia) > 0) {
|
||||||
//stuff
|
|
||||||
const ret = [];
|
const ret = [];
|
||||||
for (var i = 0; i < parseInt(NumMedia); i++) {
|
for (let i = 0; i < parseInt(NumMedia); i++) {
|
||||||
ret.push(body[`MediaUrl${i}`]);
|
ret.push(body[`MediaUrl${i}`]);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ const queries = require("../graphql-client/queries");
|
|||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
||||||
const { admin } = require("../firebase/firebase-handler");
|
const { admin } = require("../firebase/firebase-handler");
|
||||||
|
|
||||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
const gqlClient = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
exports.send = (req, res) => {
|
exports.send = (req, res) => {
|
||||||
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
||||||
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
|
} = req;
|
||||||
|
|
||||||
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
|
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
|
||||||
messagingServiceSid: messagingServiceSid,
|
messagingServiceSid: messagingServiceSid,
|
||||||
@@ -59,18 +62,36 @@ exports.send = (req, res) => {
|
|||||||
conversationid: newMessage.conversationid || ""
|
conversationid: newMessage.conversationid || ""
|
||||||
};
|
};
|
||||||
|
|
||||||
admin.messaging().send({
|
// TODO Verify
|
||||||
topic: `${imexshopid}-messaging`,
|
// const messageData = response.insert_messages.returning[0];
|
||||||
data
|
|
||||||
|
// Broadcast new message to conversation room
|
||||||
|
const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id);
|
||||||
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: r2.insert_messages.returning[0].conversation.bodyshop.id,
|
||||||
|
conversationId: r2.insert_messages.returning[0].conversation.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||||
|
isoutbound: true,
|
||||||
|
conversationId: conversationid,
|
||||||
|
updated_at: r2.insert_messages.returning[0].updated_at,
|
||||||
|
msid: message.sid,
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
ioRedis.to(conversationRoom).emit("new-message-detailed", {
|
||||||
|
newMessage: r2.insert_messages.returning[0],
|
||||||
|
conversationId: conversationid,
|
||||||
|
summary: false
|
||||||
|
});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
})
|
})
|
||||||
.catch((e2) => {
|
.catch((e2) => {
|
||||||
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
||||||
msid: message.sid,
|
msid: message.sid,
|
||||||
conversationid,
|
conversationid,
|
||||||
error: e2
|
error: e2.message,
|
||||||
|
stack: e2.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
//res.json({ success: false, message: e2 });
|
//res.json({ success: false, message: e2 });
|
||||||
@@ -80,7 +101,8 @@ exports.send = (req, res) => {
|
|||||||
//res.json({ success: false, message: error });
|
//res.json({ success: false, message: error });
|
||||||
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
||||||
conversationid,
|
conversationid,
|
||||||
error: e1
|
error: e1.message,
|
||||||
|
stack: e1.stack
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const { admin } = require("../firebase/firebase-handler");
|
|||||||
|
|
||||||
exports.status = (req, res) => {
|
exports.status = (req, res) => {
|
||||||
const { SmsSid, SmsStatus } = req.body;
|
const { SmsSid, SmsStatus } = req.body;
|
||||||
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
|
} = req;
|
||||||
client
|
client
|
||||||
.request(queries.UPDATE_MESSAGE_STATUS, {
|
.request(queries.UPDATE_MESSAGE_STATUS, {
|
||||||
msid: SmsSid,
|
msid: SmsSid,
|
||||||
@@ -21,6 +25,17 @@ exports.status = (req, res) => {
|
|||||||
msid: SmsSid,
|
msid: SmsSid,
|
||||||
fields: { status: SmsStatus }
|
fields: { status: SmsStatus }
|
||||||
});
|
});
|
||||||
|
// TODO Verify
|
||||||
|
const message = response.update_messages.returning[0];
|
||||||
|
|
||||||
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: message.conversation.bodyshopid,
|
||||||
|
conversationId: message.conversationid
|
||||||
|
});
|
||||||
|
|
||||||
|
ioRedis.to(conversationRoom).emit("message-changed", {
|
||||||
|
message
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.log("sms-status-update-error", "ERROR", "api", null, {
|
logger.log("sms-status-update-error", "ERROR", "api", null, {
|
||||||
@@ -34,18 +49,44 @@ exports.status = (req, res) => {
|
|||||||
|
|
||||||
exports.markConversationRead = async (req, res) => {
|
exports.markConversationRead = async (req, res) => {
|
||||||
const { conversationid, imexshopid } = req.body;
|
const { conversationid, imexshopid } = req.body;
|
||||||
admin.messaging().send({
|
const {
|
||||||
topic: `${imexshopid}-messaging`,
|
ioRedis,
|
||||||
// notification: {
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
// title: `ImEX Online Message - ${data.phone_num}`,
|
} = req;
|
||||||
// body: message.image_path ? `Image ${message.text}` : message.text,
|
|
||||||
// imageUrl: "https://thinkimex.com/img/logo512.png",
|
//Server side, mark the conversation as read
|
||||||
// },
|
|
||||||
data: {
|
//TODO: Convert this to run on server side. Stolen from chat-conversation.container.jsx
|
||||||
type: "messaging-mark-conversation-read",
|
// const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
|
||||||
conversationid: conversationid || ""
|
// 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 } };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id);
|
||||||
|
|
||||||
|
ioRedis.to(broadcastRoom).emit("conversation-changed", {
|
||||||
|
//type: "conversation-marked-unread" //TODO: Flush out what this looks like.
|
||||||
|
// isoutbound: true,
|
||||||
|
// conversationId: conversationid,
|
||||||
|
// updated_at: r2.insert_messages.returning[0].updated_at,
|
||||||
|
// msid: message.sid,
|
||||||
|
// summary: true
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(200);
|
res.send(200);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
const applyIOHelpers = ({ app, api, io, logger }) => {
|
const applyIOHelpers = ({ app, api, io, logger }) => {
|
||||||
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
||||||
|
// Messaging - conversation specific room to handle detailed messages when the user has a conversation open.
|
||||||
|
const getBodyshopConversationRoom = ({bodyshopId, conversationId}) =>
|
||||||
|
`bodyshop-conversation-room:${bodyshopId}:${conversationId}`;
|
||||||
|
|
||||||
const ioHelpersAPI = {
|
const ioHelpersAPI = {
|
||||||
getBodyshopRoom
|
getBodyshopRoom,
|
||||||
|
getBodyshopConversationRoom
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper middleware
|
// Helper middleware
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
const { admin } = require("../firebase/firebase-handler");
|
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 = ({
|
const redisSocketEvents = ({
|
||||||
io,
|
io,
|
||||||
redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis
|
redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis
|
||||||
ioHelpers: { getBodyshopRoom },
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||||||
logger
|
logger
|
||||||
}) => {
|
}) => {
|
||||||
// Logging helper functions
|
// Logging helper functions
|
||||||
@@ -113,6 +121,7 @@ const redisSocketEvents = ({
|
|||||||
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
||||||
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disconnect Events
|
// Disconnect Events
|
||||||
const registerDisconnectEvents = (socket) => {
|
const registerDisconnectEvents = (socket) => {
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
@@ -129,10 +138,37 @@ const redisSocketEvents = ({
|
|||||||
|
|
||||||
socket.on("disconnect", disconnect);
|
socket.on("disconnect", disconnect);
|
||||||
};
|
};
|
||||||
|
// Messaging Events
|
||||||
|
const registerMessagingEvents = (socket) => {
|
||||||
|
const joinConversationRoom = async ({ bodyshopId, conversationId }) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopConversationRoom({ bodyshopId, conversationId });
|
||||||
|
socket.join(room);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("error", "Failed to join conversation", error);
|
||||||
|
socket.emit("error", { message: "Failed to join conversation" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaveConversationRoom = ({ bodyshopId, conversationId }) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopConversationRoom({ bodyshopId, 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("join-bodyshop-conversation", joinConversationRoom);
|
||||||
|
socket.on("leave-bodyshop-conversation", leaveConversationRoom);
|
||||||
|
};
|
||||||
|
|
||||||
// Call Handlers
|
// Call Handlers
|
||||||
registerRoomAndBroadcastEvents(socket);
|
registerRoomAndBroadcastEvents(socket);
|
||||||
registerUpdateEvents(socket);
|
registerUpdateEvents(socket);
|
||||||
|
registerMessagingEvents(socket);
|
||||||
registerDisconnectEvents(socket);
|
registerDisconnectEvents(socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user