feature/IO-3000-Migrate-MSG-to-Sockets - Progress Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -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,6 +47,7 @@ function ChatConversationListComponent({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||||
const { socket, clientId } = useContext(SocketContext);
|
const { socket } = useContext(SocketContext);
|
||||||
const [conversationDetails, setConversationDetails] = useState({});
|
const [conversationDetails, setConversationDetails] = useState({});
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error] = useState(null);
|
||||||
|
|
||||||
// Fetch conversation details and messages when a conversation is selected
|
// Fetch conversation details and messages when a conversation is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -28,8 +28,6 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
|
|
||||||
socket.on("conversation-details", (data) => {
|
socket.on("conversation-details", (data) => {
|
||||||
setConversationDetails(data.conversation);
|
setConversationDetails(data.conversation);
|
||||||
console.log("HIT HIT HIT");
|
|
||||||
console.dir(data);
|
|
||||||
setMessages(data.messages);
|
setMessages(data.messages);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
behavior: "smooth" // Smooth scrolling
|
||||||
|
});
|
||||||
|
}, 50); // Slight delay to ensure layout recalculates
|
||||||
|
}
|
||||||
|
}, [messages]); // Triggered when new messages are added
|
||||||
|
|
||||||
useEffect(scrollToBottom, [messages]);
|
const renderMessage = (index) => {
|
||||||
|
const message = messages[index];
|
||||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
|
||||||
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;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function ChatPopupComponent({
|
|||||||
unreadCount
|
unreadCount
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { socket, clientId } = useContext(SocketContext);
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
// Emit event to open messaging when chat becomes visible
|
// Emit event to open messaging when chat becomes visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,55 +1,66 @@
|
|||||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||||
import { Input, Spin } from "antd";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
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 { 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 ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
isSending: selectIsSending,
|
isSending: selectIsSending,
|
||||||
message: selectMessage
|
message: selectMessage,
|
||||||
|
user: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
sendMessage: (message) => dispatch(sendMessage(message)),
|
|
||||||
setMessage: (message) => dispatch(setMessage(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 inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||||
|
|
||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
sendMessage({
|
const messageData = {
|
||||||
|
user,
|
||||||
to: conversation.phone_num,
|
to: conversation.phone_num,
|
||||||
body: message || "",
|
body: message || "",
|
||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Emit the send-message event via WebSocket
|
||||||
|
socket.emit("send-message", messageData);
|
||||||
|
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
selectedMedia.map((i) => {
|
selectedMedia.map((i) => {
|
||||||
return { ...i, isSelected: false };
|
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)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
onPressEnter={(event) => {
|
onPressEnter={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!!!event.shiftKey) handleEnter();
|
if (!event.shiftKey) handleEnter();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<SendOutlined
|
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
||||||
className="imex-flex-row__margin"
|
|
||||||
// disabled={message === "" || !message}
|
|
||||||
onClick={handleEnter}
|
|
||||||
/>
|
|
||||||
<Spin
|
<Spin
|
||||||
style={{ display: `${isSending ? "" : "none"}` }}
|
style={{ display: `${isSending ? "" : "none"}` }}
|
||||||
indicator={
|
indicator={
|
||||||
|
|||||||
@@ -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()
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
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 { MARK_MESSAGES_AS_READ, GET_CONVERSATIONS, GET_CONVERSATION_DETAILS } = require("../graphql-client/queries");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
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 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
|
||||||
@@ -173,7 +180,96 @@ const redisSocketEvents = ({
|
|||||||
socket.emit("error", { message: "Failed to mark messages as read" });
|
socket.emit("error", { message: "Failed to mark messages as read" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Mark Messages as Read
|
|
||||||
|
const sendMessage = (data) => {
|
||||||
|
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid, user } = data;
|
||||||
|
console.dir({ 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) => {
|
||||||
|
//console.log("Responding GQL Message ID", JSON.stringify(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) : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("send-message", sendMessage);
|
||||||
socket.on("mark-as-read", markAsRead);
|
socket.on("mark-as-read", markAsRead);
|
||||||
socket.on("join-conversation", joinConversation);
|
socket.on("join-conversation", joinConversation);
|
||||||
socket.on("open-messaging", openMessaging);
|
socket.on("open-messaging", openMessaging);
|
||||||
|
|||||||
Reference in New Issue
Block a user