Files
Dwinzo_Demo/app/src/components/ui/collaboration/threads/ThreadChat.tsx

384 lines
16 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { CloseIcon, KebabIcon } from "../../../icons/ExportCommonIcons";
import { ExpandIcon } from "../../../icons/SimulationIcons";
import { adjustHeight } from "../function/textAreaHeightAdjust";
import { useSelectedComment } from "../../../../store/builder/store";
import { useSocketStore } from "../../../../store/socket/useSocketStore";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import Messages from "./Messages";
import ThreadSocketResponsesDev from "../../../../modules/collaboration/socket/threadSocketResponses.dev";
import { getUserData } from "../../../../functions/getUserData";
import { addCommentsApi } from "../../../../services/factoryBuilder/collab/comments/addCommentApi";
import { deleteThreadApi } from "../../../../services/factoryBuilder/collab/comments/deleteThreadApi";
import { createThreadApi } from "../../../../services/factoryBuilder/collab/comments/createThreadApi";
import { getRelativeTime } from "../function/getRelativeTime";
const ThreadChat: React.FC = () => {
const { userId, organization } = getUserData();
const [openThreadOptions, setOpenThreadOptions] = useState(false);
const [inputActive, setInputActive] = useState(false);
const [value, setValue] = useState<string>("");
const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState, position2Dstate } = useSelectedComment();
const [mode, setMode] = useState<"create" | "edit" | null>("create");
const [isEditable, setIsEditable] = useState(false);
const [editedThread, setEditedThread] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<Reply[]>([]);
const { projectId } = useParams();
const [dragging, setDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState({ x: position2Dstate.x, y: position2Dstate.y });
const { threadSocket } = useSocketStore();
const modeRef = useRef<"create" | "edit" | null>(null);
const messagesRef = useRef<HTMLDivElement>(null);
const { versionStore, threadStore } = useSceneContext();
const { selectedVersion } = versionStore();
const { addThread, removeThread, addReply, threads } = threadStore();
useEffect(() => {
modeRef.current = mode;
}, [mode]);
useEffect(() => {
if (threads.length > 0 && selectedComment) {
const allMessages = threads
.flatMap((val: any) => (val?.threadId === selectedComment?.threadId ? val.comments : []))
.map((c) => {
return {
replyId: c._id ?? c.replyId,
creatorId: c.creatorId || c.userId,
createdAt: c.createdAt,
lastUpdatedAt: "1 hr ago",
comment: c.comment,
_id: c._id ?? c.replyId,
};
});
setMessages(allMessages);
}
}, [selectedComment]);
useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current);
}, [value]);
const clamp = (val: number, min: number, max: number) => {
return Math.min(Math.max(val, min), max);
};
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
const target = event.target as HTMLElement;
if (
target.closest("button") ||
target.closest(".sent-button") ||
target.closest("textarea") ||
target.closest(".options-button") ||
target.closest(".options-list") ||
target.closest(".send-message-wrapper") ||
target.closest(".options delete")
) {
return;
}
const wrapper = wrapperRef.current;
if (!wrapper) return;
const rect = wrapper.getBoundingClientRect();
const offsetX = event.clientX - rect.left;
const offsetY = event.clientY - rect.top;
setDragging(true);
setDragOffset({ x: offsetX, y: offsetY });
wrapper.setPointerCapture(event.pointerId);
};
const updatePosition = ({ clientX, clientY }: { clientX: number; clientY: number }, allowMove: boolean = true) => {
if (!allowMove || !wrapperRef.current) return;
const container = document.getElementById("work-space-three-d-canvas");
const wrapper = wrapperRef.current;
if (!container || !wrapper) return;
const containerRect = container.getBoundingClientRect();
let newX = clientX - containerRect.left - dragOffset.x;
let newY = clientY - containerRect.top - dragOffset.y;
const maxX = containerRect.width - wrapper.offsetWidth;
const maxY = containerRect.height - wrapper.offsetHeight;
newX = clamp(newX, 0, maxX);
newY = clamp(newY, 0, maxY);
setPosition({ x: newX, y: newY });
};
const handlePointerMove = (e: { clientX: number; clientY: number }) => {
if (dragging) updatePosition(e, true);
};
// Commented this useEffect to prevent offset after user saved the comment
// useEffect(() => {
// updatePosition({ clientX: position.x, clientY: position.y }, true);
// }, [selectedComment]);
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return;
setDragging(false);
const wrapper = wrapperRef.current;
if (wrapper) wrapper.releasePointerCapture(event.pointerId);
};
const handleCreateComments = async (e: any) => {
e.preventDefault();
if (!value) return;
if (!threadSocket?.connected && mode === "create") {
// API
addCommentsApi(projectId, value, selectedComment?.threadId, selectedVersion?.versionId || "").then((createComments) => {
if (createComments.message === "Thread comments add Successfully" && createComments.data) {
const commentData = {
replyId: `${createComments.data?._id}`,
creatorId: `${selectedComment?.threadId}`,
createdAt: "2 hrs ago",
lastUpdatedAt: "2 hrs ago",
comment: value,
};
setMessages((prevMessages) => [...prevMessages, commentData]);
addReply(selectedComment?.threadId, commentData);
}
});
} else if (threadSocket?.connected && mode === "create") {
// SOCKET
const addThread = {
versionId: selectedVersion?.versionId || "",
projectId,
userId,
comment: value,
organization,
threadId: selectedComment?.threadId,
};
threadSocket.emit("v1-Comment:add", addThread);
}
setInputActive(false);
};
const handleDeleteThread = async () => {
if (!projectId) return;
if (!threadSocket?.connected) {
// API
deleteThreadApi(projectId, selectedComment?.threadId, selectedVersion?.versionId || "").then((deleteThread) => {
if (deleteThread.message === "Thread deleted Successfully") {
removeThread(selectedComment?.threadId);
setSelectedComment(null);
}
});
} else {
// SOCKET
const deleteThread = {
projectId,
userId,
organization,
threadId: selectedComment?.threadId,
versionId: selectedVersion?.versionId || "",
};
setSelectedComment(null);
removeThread(selectedComment?.threadId);
threadSocket.emit("v1:thread:delete", deleteThread);
}
};
const handleCreateThread = async (e: any) => {
e.preventDefault();
if (!projectId) return;
if (!threadSocket?.connected) {
// API
createThreadApi(projectId, "active", commentPositionState.position, [0, 0, 0], value, selectedVersion?.versionId || "").then((thread) => {
if (thread.message === "Thread created Successfully" && thread?.threadData) {
const comment: ThreadSchema = {
state: "active",
threadId: thread?.threadData?._id,
creatorId: userId,
createdAt: getRelativeTime(thread.threadData?.createdAt),
threadTitle: value,
lastUpdatedAt: new Date().toISOString(),
position: commentPositionState.position,
rotation: [0, 0, 0],
comments: [],
};
addThread(comment);
setCommentPositionState(null);
setInputActive(false);
setSelectedComment(null);
}
});
} else {
// SOCKET
const createThread = {
projectId,
versionId: selectedVersion?.versionId || "",
userId,
organization,
state: "active",
position: commentPositionState.position,
rotation: [0, 0, 0],
threadTitle: value,
};
setCommentPositionState(null);
setInputActive(false);
setSelectedComment(null);
threadSocket.emit("v1:thread:create", createThread);
}
};
const scrollToBottom = () => {
const messagesWrapper = document.querySelector(".messages-wrapper");
if (messagesWrapper) {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight;
}
};
useEffect(() => {
if (messages.length > 0) scrollToBottom();
}, [messages]);
return (
<div
ref={wrapperRef}
className="thread-chat-wrapper"
onPointerDown={handlePointerDown}
onPointerMove={(e) => handlePointerMove({ clientX: e.clientX, clientY: e.clientY })}
onPointerUp={handlePointerUp}
style={{
position: "absolute",
left: position.x,
top: position.y,
cursor: dragging ? "grabbing" : "grab",
userSelect: "none",
zIndex: 9999,
}}
>
<div className="thread-chat-container">
<div className="header-wrapper">
<div className="header">Comment</div>
<div className="header-options">
<div
className="options-button"
style={{ cursor: "pointer" }}
onClick={(e) => {
e.preventDefault();
setOpenThreadOptions((prev) => !prev);
}}
>
<KebabIcon />
</div>
{openThreadOptions && (
<div className="options-list">
<div className="options">Mark as Unread</div>
<div className="options">Mark as Resolved</div>
<div className="options delete" onClick={handleDeleteThread}>
Delete Thread
</div>
</div>
)}
<button
className="close-button"
onClick={() => {
setSelectedComment(null);
setCommentPositionState(null);
}}
>
<CloseIcon />
</button>
</div>
</div>
<div className="messages-wrapper" ref={messagesRef}>
{selectedComment && <Messages val={selectedComment} i={1} key={selectedComment.creatorId} isEditableThread={true} setEditedThread={setEditedThread} editedThread={editedThread} />}
{messages.map((val, i) => (
<Messages
val={val as any}
i={i}
key={val.replyId}
setMessages={setMessages}
setIsEditable={setIsEditable}
isEditable={isEditable}
isEditableThread={false}
setMode={setMode}
mode={mode}
/>
))}
</div>
<div className="send-message-wrapper">
<div className={`input-container ${inputActive ? "active" : ""}`}>
<textarea
placeholder={commentPositionState && selectedComment === null ? "Type Thread Title" : "type something"}
ref={textareaRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (commentPositionState && selectedComment === null) {
handleCreateThread(e);
} else {
setMode("create");
handleCreateComments(e);
textareaRef.current?.blur();
}
setValue("");
}
if (e.key === "Escape") {
textareaRef.current?.blur();
}
}}
onClick={() => {
if (!commentPositionState && selectedComment !== null) {
setMode("create");
}
}}
autoFocus={selectedComment === null}
onBlur={() => setInputActive(false)}
onFocus={() => setInputActive(true)}
style={{ resize: "none" }}
/>
<div
className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}
onClick={(e) => {
if (commentPositionState && selectedComment === null) {
handleCreateThread(e);
} else {
setMode("create");
handleCreateComments(e);
}
setValue("");
}}
>
<ExpandIcon />
</div>
</div>
</div>
</div>
<ThreadSocketResponsesDev setMessages={setMessages} modeRef={modeRef} messages={messages} />
</div>
);
};
export default ThreadChat;