feature/IO-3000-messaging-sockets-migration2 -

- Various work

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-11-28 09:10:23 -08:00
parent ad1761096a
commit 08c0da1bed
6 changed files with 171 additions and 146 deletions

View File

@@ -40,7 +40,7 @@ export function ChatArchiveButton({ conversation, bodyshop }) {
}; };
return ( return (
<Button onClick={handleToggleArchive} loading={loading} type="primary"> <Button onClick={handleToggleArchive} loading={loading} className="archive-button" type="primary">
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")} {conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
</Button> </Button>
); );

View File

@@ -1,53 +1,85 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { renderMessage } from "./renderMessage"; import { renderMessage } from "./renderMessage";
import "./chat-message-list.styles.scss"; import "./chat-message-list.styles.scss";
const SCROLL_DELAY_MS = 50;
const INITIAL_SCROLL_DELAY_MS = 100;
export default function ChatMessageListComponent({ messages }) { export default function ChatMessageListComponent({ messages }) {
const virtuosoRef = useRef(null); const virtuosoRef = useRef(null);
const [atBottom, setAtBottom] = useState(true);
const loadedImagesRef = useRef(0);
// Scroll to the bottom after a short delay when the component mounts const handleScrollStateChange = (isAtBottom) => {
setAtBottom(isAtBottom);
};
const resetImageLoadState = () => {
loadedImagesRef.current = 0;
};
const preloadImages = (imagePaths, onComplete) => {
resetImageLoadState();
if (imagePaths.length === 0) {
onComplete();
return;
}
imagePaths.forEach((url) => {
const img = new Image();
img.src = url;
img.onload = img.onerror = () => {
loadedImagesRef.current += 1;
if (loadedImagesRef.current === imagePaths.length) {
onComplete();
}
};
});
};
// Ensure all images are loaded on initial render
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const imagePaths = messages
if (virtuosoRef?.current?.scrollToIndex && messages?.length) { .filter((message) => message.image && message.image_path?.length > 0)
.flatMap((message) => message.image_path);
preloadImages(imagePaths, () => {
if (virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({ virtuosoRef.current.scrollToIndex({
index: messages.length - 1, index: messages.length - 1,
behavior: "auto" // Instantly scroll to the bottom align: "end",
behavior: "auto"
}); });
} }
}, INITIAL_SCROLL_DELAY_MS); });
}, [messages]);
// Cleanup the timeout on unmount // Handle scrolling when new messages are added
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // ESLint is disabled for this line because we only want this to load once (valid exception)
// Scroll to the bottom after the new messages are rendered
useEffect(() => { useEffect(() => {
if (virtuosoRef?.current?.scrollToIndex && messages?.length) { if (!atBottom) return;
const timeout = setTimeout(() => {
const latestMessage = messages[messages.length - 1];
const imagePaths = latestMessage?.image_path || [];
preloadImages(imagePaths, () => {
if (virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({ virtuosoRef.current.scrollToIndex({
index: messages.length - 1, index: messages.length - 1,
align: "end", // Ensure the last message is fully visible align: "end",
behavior: "smooth" // Smooth scrolling behavior: "smooth"
}); });
}, SCROLL_DELAY_MS); // Slight delay to ensure layout recalculates }
});
// Cleanup timeout on dependency changes }, [messages, atBottom]);
return () => clearTimeout(timeout);
}
}, [messages]); // Triggered when new messages are added
return ( return (
<div className="chat"> <div className="chat">
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
data={messages} data={messages}
itemContent={(index) => renderMessage(messages, index)} // Pass `messages` to renderMessage overscan={!!messages.reduce((acc, message) => acc + (message.image_path?.length || 0), 0) ? messages.length : 0}
followOutput="smooth" // Ensure smooth scrolling when new data is appended itemContent={(index) => renderMessage(messages, index)}
followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)}
initialTopMostItemIndex={messages.length - 1}
style={{ height: "100%", width: "100%" }} style={{ height: "100%", width: "100%" }}
/> />
</div> </div>

View File

@@ -1,110 +1,125 @@
.message-icon {
color: whitesmoke;
border: #000000;
position: absolute;
margin: 0 0.1rem;
bottom: 0.1rem;
right: 0.3rem;
z-index: 5;
}
.chat { .chat {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0.8rem 0rem; height: 100%;
overflow: hidden; // Ensure the content scrolls correctly width: 100%;
}
.archive-button {
height: 20px;
border-radius: 4px;
} }
.messages { .messages {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5rem; // Add padding to avoid edge clipping padding: 0.5rem; // Prevent edge clipping
} }
.message { .message {
position: relative;
border-radius: 20px; border-radius: 20px;
padding: 0.25rem 0.8rem; padding: 0.25rem 0.8rem;
word-wrap: break-word;
.message-img { &-img {
max-width: 10rem; max-width: 10rem;
max-height: 10rem; max-height: 10rem;
object-fit: contain; object-fit: contain;
margin: 0.2rem; margin: 0.2rem;
border-radius: 4px;
}
&-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
} }
} }
.yours { .message-icon {
align-items: flex-start; position: absolute;
bottom: 0.1rem;
right: 0.3rem;
margin: 0 0.1rem;
color: whitesmoke;
z-index: 5;
} }
.msgmargin { .msgmargin {
margin-top: 0.1rem; margin: 0.1rem 0;
margin-bottom: 0.1rem;
} }
.yours .message { .yours,
margin-right: 20%; .mine {
background-color: #eee; display: flex;
position: relative; flex-direction: column;
.message {
position: relative;
&:last-child:before,
&:last-child:after {
content: "";
position: absolute;
bottom: 0;
height: 20px;
width: 20px;
z-index: 0;
}
&:last-child:after {
width: 10px;
background: white;
z-index: 1;
}
}
} }
.yours .message:last-child:before { /* "Yours" (incoming) message styles */
content: ""; .yours {
position: absolute; align-items: flex-start;
z-index: 0;
bottom: 0; .message {
left: -7px; margin-right: 20%;
height: 20px; background-color: #eee;
width: 20px;
background: #eee; &:last-child:before {
border-bottom-right-radius: 15px; left: -7px;
} background: #eee;
border-bottom-right-radius: 15px;
.yours .message:last-child:after { }
content: "";
position: absolute; &:last-child:after {
z-index: 1; left: -10px;
bottom: 0; border-bottom-right-radius: 10px;
left: -10px; }
width: 10px; }
height: 20px;
background: white;
border-bottom-right-radius: 10px;
} }
/* "Mine" (outgoing) message styles */
.mine { .mine {
align-items: flex-end; align-items: flex-end;
.message {
color: white;
margin-left: 25%;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
padding-bottom: 0.6rem;
&:last-child:before {
right: -8px;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
border-bottom-left-radius: 15px;
}
&:last-child:after {
right: -10px;
border-bottom-left-radius: 10px;
}
}
} }
.mine .message { .virtuoso-container {
color: white; flex: 1;
margin-left: 25%; overflow: auto;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
position: relative;
padding-bottom: 0.6rem;
}
.mine .message:last-child:before {
content: "";
position: absolute;
z-index: 0;
bottom: 0;
right: -8px;
height: 20px;
width: 20px;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
border-bottom-left-radius: 15px;
}
.mine .message:last-child:after {
content: "";
position: absolute;
z-index: 1;
bottom: 0;
right: -10px;
width: 10px;
height: 20px;
background: white;
border-bottom-left-radius: 10px;
} }

View File

@@ -7,28 +7,38 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
export const renderMessage = (messages, index) => { export const renderMessage = (messages, index) => {
const message = messages[index]; const message = messages[index];
return ( return (
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}> <div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
<div className="message msgmargin"> <div className="message msgmargin">
<Tooltip title={DateTimeFormatter({ children: message.created_at })}> <Tooltip title={DateTimeFormatter({ children: message.created_at })}>
<div> <div>
{message.image_path && {/* Render images if available */}
message.image_path.map((i, idx) => ( {message.image && message.image_path?.length > 0 && (
<div key={idx} style={{ display: "flex", justifyContent: "center" }}> <div className="message-images">
<a href={i} target="__blank" rel="noopener noreferrer"> {message.image_path.map((url, idx) => (
<img alt="Received" className="message-img" src={i} /> <div key={idx} style={{ display: "flex", justifyContent: "center" }}>
</a> <a href={url} target="_blank" rel="noopener noreferrer">
</div> <img alt="Received" className="message-img" src={url} />
))} </a>
<div>{message.text}</div> </div>
))}
</div>
)}
{/* Render text if available */}
{message.text && <div>{message.text}</div>}
</div> </div>
</Tooltip> </Tooltip>
{/* Message status icons */}
{message.status && (message.status === "sent" || message.status === "delivered") && ( {message.status && (message.status === "sent" || message.status === "delivered") && (
<div className="message-status"> <div className="message-status">
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" /> <Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
</div> </div>
)} )}
</div> </div>
{/* Outbound message metadata */}
{message.isoutbound && ( {message.isoutbound && (
<div style={{ fontSize: 10 }}> <div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", { {i18n.t("messaging.labels.sentby", {

View File

@@ -86,9 +86,10 @@ export function ChatTagRoContainer({ conversation, bodyshop }) {
handleSearch={handleSearch} handleSearch={handleSearch}
handleInsertTag={handleInsertTag} handleInsertTag={handleInsertTag}
setOpen={setOpen} setOpen={setOpen}
style={{ cursor: "pointer" }}
/> />
) : ( ) : (
<Tag onClick={() => setOpen(true)}> <Tag style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
<PlusOutlined /> <PlusOutlined />
{t("messaging.actions.link")} {t("messaging.actions.link")}
</Tag> </Tag>

View File

@@ -149,39 +149,6 @@ const cache = new InMemoryCache({
fields: { fields: {
conversations: offsetLimitPagination() conversations: offsetLimitPagination()
} }
},
conversations: {
fields: {
job_conversations: {
keyArgs: false, // Indicates that all job_conversations share the same key
merge(existing = [], incoming) {
// Merge existing and incoming job_conversations
const merged = [
...existing,
...incoming.filter(
(incomingItem) => !existing.some((existingItem) => existingItem.__ref === incomingItem.__ref)
)
];
return merged;
}
},
messages: {
keyArgs: false, // Ignore arguments when determining uniqueness (like `order_by`).
merge(existing = [], incoming = [], { readField }) {
const existingIds = new Set(existing.map((message) => readField("id", message)));
// Merge incoming messages, avoiding duplicates
const merged = [...existing];
incoming.forEach((message) => {
if (!existingIds.has(readField("id", message))) {
merged.push(message);
}
});
return merged;
}
}
}
} }
} }
}); });