Files
Dwinzo_Demo/app/src/components/ui/collaboration/ThreadChat.tsx
2025-06-23 09:37:53 +05:30

391 lines
13 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
import Messages from "./Messages";
import { ExpandIcon } from "../../icons/SimulationIcons";
import { adjustHeight } from "./function/textAreaHeightAdjust";
import { useParams } from "react-router-dom";
import { useSelectedComment, useSocketStore } from "../../../store/builder/store";
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
import { getUserData } from "../../../functions/getUserData";
import ThreadSocketResponsesDev from "../../../modules/collaboration/socket/threadSocketResponses.dev";
import { addCommentsApi } from "../../../services/factoryBuilder/comments/addCommentsApi";
import { deleteThreadApi } from "../../../services/factoryBuilder/comments/deleteThreadApi";
import { createThreadApi } from "../../../services/factoryBuilder/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 { addComment, removeReply, removeComment, addReply, comments } = useCommentStore();
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 [selectedDiv, setSelectedDiv] = useState(true);
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);
useEffect(() => {
modeRef.current = mode;
}, [mode]);
useEffect(() => {
if (comments.length > 0 && selectedComment) {
const allMessages = comments
.flatMap((val: any) =>
val?.threadId === selectedComment?.threadId ? val.comments : []
)
.map((c) => {
return {
replyId: c._id ?? "",
creatorId: c.creatorId || c.userId,
createdAt: c.createdAt,
lastUpdatedAt: "1 hr ago",
comment: c.comment,
_id: c._id ?? "",
};
});
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;
// Avoid dragging if a button, icon, textarea etc. was clicked
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);
};
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();
try {
// const createComments = await addCommentsApi(projectId, value, selectedComment?.threadId)/
// 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)
// }
if (threadSocket && mode === "create") {
const addComment = {
projectId,
userId,
comment: value,
organization,
threadId: selectedComment?.threadId
}
threadSocket.emit("v1-Comment:add", addComment);
}
} catch {
}
setInputActive(false)
}
const handleDeleteThread = async () => {
if (!projectId) return;
try {
// const deleteThread = await deleteThreadApi(projectId, selectedComment?.threadId)
//
// if (deleteThread.message === "Thread deleted Successfully") {
// removeComment(selectedComment?.threadId)
// setSelectedComment([])
// }
if (threadSocket) {
// projectId, userId, organization, threadId
const deleteThread = {
projectId,
userId,
organization,
threadId: selectedComment?.threadId
}
setSelectedComment(null)
removeComment(selectedComment?.threadId)
threadSocket.emit("v1:thread:delete", deleteThread);
}
}
catch {
}
}
const handleCreateThread = async (e: any) => {
e.preventDefault();
if (!projectId) return;
try {
// try {
// const thread = await createThreadApi(
// projectId,
// "active",
// commentPositionState[0].position,
// [0, 0, 0],
// value
// );
//
//
// if (thread.message === "Thread created Successfully" && thread?.threadData) {
//
// const comment: CommentSchema = {
// state: 'active',
// threadId: thread?.threadData?._id,
// creatorId: userId,
// createdAt: getRelativeTime(thread.threadData?.createdAt),
// threadTitle: value,
// lastUpdatedAt: new Date().toISOString(),
// position: commentPositionState[0].position,
// rotation: [0, 0, 0],
// comments: []
// }
// addComment(comment);
// setCommentPositionState(null);
// setInputActive(false);
// setSelectedComment([])
// }
const createThread = {
projectId,
userId,
organization,
state: "active",
position: commentPositionState.position,
rotation: [0, 0, 0],
threadTitle: value
};
if (threadSocket) {
setInputActive(false);
threadSocket.emit("v1:thread:create", createThread);
}
} catch (err) {
}
};
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 && 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;