Merge remote-tracking branch 'origin/feature/threaded-comments' into v3

This commit is contained in:
Jerald-Golden-B 2025-06-21 09:21:55 +05:30
commit 78e57ab9fa
20 changed files with 1169 additions and 234 deletions

View File

@ -1,13 +1,21 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
import { getUserData } from "../../../functions/getUserData";
import { getAllThreads } from "../../../services/factoryBuilder/comments/getAllThreads";
import { useParams } from "react-router-dom";
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
import { getRelativeTime } from "./function/getRelativeTime";
import { useSelectedComment } from "../../../store/builder/store";
interface CommentThreadsProps { interface CommentThreadsProps {
commentClicked: () => void; commentClicked: () => void;
comment?: CommentSchema
} }
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => { const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, comment }) => {
const [expand, setExpand] = useState(false); const [expand, setExpand] = useState(false);
const commentsedUsers = [{ creatorId: "1" }]; const commentsedUsers = [{ creatorId: "1" }];
const { userName } = getUserData();
const CommentDetails = { const CommentDetails = {
state: "active", state: "active",
@ -16,26 +24,26 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
createdAt: "2 hours ago", createdAt: "2 hours ago",
comment: "Thread check", comment: "Thread check",
lastUpdatedAt: "string", lastUpdatedAt: "string",
replies: [ comments: [
{ {
replyId: "string", replyId: "string",
creatorId: "string", creatorId: "string",
createdAt: "string", createdAt: "string",
lastUpdatedAt: "string", lastUpdatedAt: "string",
reply: "string", comment: "string",
}, },
{ {
replyId: "string", replyId: "string",
creatorId: "string", creatorId: "string",
createdAt: "string", createdAt: "string",
lastUpdatedAt: "string", lastUpdatedAt: "string",
reply: "string", comment: "string",
}, },
], ],
}; };
function getUsername(userId: string) { function getUsername(userId: string) {
const UserName = "username"; const UserName = userName?.charAt(0).toUpperCase() || "user";
return UserName; return UserName;
} }
@ -48,15 +56,15 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
} }
} }
return ( return (
<div className="comments-threads-wrapper"> <div className="comments-threads-wrapper">
<button <button
onPointerEnter={() => getDetails()} onPointerEnter={() => getDetails()}
onPointerLeave={() => getDetails()} onPointerLeave={() => getDetails()}
onClick={() => getDetails("clicked")} onClick={() => getDetails("clicked")}
className={`comments-threads-container ${ className={`comments-threads-container ${expand ? "open" : "closed"
expand ? "open" : "closed" } unread`}
} unread`}
> >
<div className="users-commented"> <div className="users-commented">
{commentsedUsers.map((val, i) => ( {commentsedUsers.map((val, i) => (
@ -70,24 +78,37 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
{getUsername(val.creatorId)[0]} {getUsername(val.creatorId)[0]}
</div> </div>
))} ))}
{/* {commentsedUsers.map((val, i) => (
<div
className="users"
key={val.creatorId}
style={{
background: getAvatarColor(i, getUsername(val.creatorId)),
}}
>
{getUsername(val.creatorId)[0]}
</div>
))} */}
</div> </div>
<div className={`last-comment-details ${expand ? "expand" : ""}`}> <div className={`last-comment-details ${expand ? "expand" : ""}`}>
<div className="header"> <div className="header">
<div className="user-name"> <div className="user-name">
{getUsername(CommentDetails.creatorId)} {userName}
{/* {getUsername(CommentDetails.creatorId)} */}
</div> </div>
<div className="time">{CommentDetails.createdAt}</div> <div className="time">{comment?.createdAt && getRelativeTime(comment.createdAt)}</div>
</div> </div>
<div className="message">{CommentDetails.comment}</div> <div className="message">{comment?.threadTitle}</div>
{CommentDetails.replies.length > 0 && ( {comment && comment?.comments.length > 0 && (
<div className="replies"> <div className="comments">
{CommentDetails.replies.length}{" "} {comment && comment?.comments.length}{" "}
{CommentDetails.replies.length === 1 ? "reply" : "replies"} {comment && comment?.comments.length === 1 ? "comment" : "replies"}
</div> </div>
)} )}
</div> </div>
</button> </button>
</div> </div>
); );
}; };

View File

@ -2,44 +2,181 @@ import React, { useEffect, useRef, useState } from "react";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
import { KebabIcon } from "../../icons/ExportCommonIcons"; import { KebabIcon } from "../../icons/ExportCommonIcons";
import { adjustHeight } from "./function/textAreaHeightAdjust"; import { adjustHeight } from "./function/textAreaHeightAdjust";
import { getUserData } from "../../../functions/getUserData";
import { useParams } from "react-router-dom";
import { deleteCommentApi } from "../../../services/factoryBuilder/comments/deleteCommentApi";
import { addCommentsApi } from "../../../services/factoryBuilder/comments/addCommentsApi";
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
import { useSelectedComment, useSocketStore } from "../../../store/builder/store";
import { getRelativeTime } from "./function/getRelativeTime";
import { editThreadTitleApi } from "../../../services/factoryBuilder/comments/editThreadTitleApi";
interface MessageProps { interface MessageProps {
val: Reply | CommentSchema; val: Reply | CommentSchema;
// val: Reply | CommentSchema;
i: number; i: number;
setMessages?: React.Dispatch<React.SetStateAction<Reply[]>>
setIsEditable?: React.Dispatch<React.SetStateAction<boolean>>
setEditedThread?: React.Dispatch<React.SetStateAction<boolean>>
setMode?: React.Dispatch<React.SetStateAction<'create' | 'edit' | null>>
isEditable?: boolean;
isEditableThread?: boolean
editedThread?: boolean;
mode?: 'create' | 'edit' | null
} }
const Messages: React.FC<MessageProps> = ({ val, i }) => { const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEditable, setEditedThread, editedThread, isEditableThread, setMode }) => {
const [isEditing, setIsEditing] = useState(false);
const { comments, updateComment, updateReply, removeReply } = useCommentStore();
const [openOptions, setOpenOptions] = useState(false); const [openOptions, setOpenOptions] = useState(false);
const { projectId } = useParams();
const { threadSocket } = useSocketStore();
const { userName, userId, organization } = getUserData();
const [isEditComment, setIsEditComment] = useState(false)
const { selectedComment, setCommentPositionState } = useSelectedComment();
// input // input
const [value, setValue] = useState<string>( const [value, setValue] = useState<string>(
"reply" in val ? val.reply : val.comment "comment" in val ? val.comment : val.threadTitle
); );
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const currentUser = "1"; const currentUser = "1";
const UserName = "username"; // const UserName = "username";
useEffect(() => { useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current); if (textareaRef.current) adjustHeight(textareaRef.current);
}, [value]); }, [value]);
function handleCancelAction() { function handleCancelAction() {
setIsEditing(false); setCommentPositionState(null)
setIsEditable && setIsEditable(true);
setIsEditComment(false)
} }
function handleSaveAction() { const handleSaveAction = async () => {
setIsEditing(false);
if (!projectId) return
if (isEditableThread && editedThread) {
try {
// const editThreadTitle = await editThreadTitleApi(projectId, (val as CommentSchema).threadId, value)
// if (editThreadTitle.message == "ThreadTitle updated Successfully") {
// const editedThread: CommentSchema = {
// state: 'active',
// threadId: editThreadTitle.data.replyId,
// creatorId: userId,
// createdAt: getRelativeTime(editThreadTitle.data.createdAt),
// threadTitle: value,
// lastUpdatedAt: new Date().toISOString(),
// position: editThreadTitle.data.position,
// rotation: [0, 0, 0],
// comments: []
// }
// updateComment((val as CommentSchema).threadId, editedThread)
// }
// projectId, userId, threadTitle, organization, threadId
const threadEdit = {
projectId,
userId,
threadTitle: value,
organization,
threadId: (val as CommentSchema).threadId
}
threadSocket.emit('v1:thread:updateTitle', threadEdit)
} catch {
}
} else {
if (mode === "edit") {
try {
// const editComments = await addCommentsApi(projectId, value, selectedComment?.threadId, (val as Reply).replyId)
//
// const commentData = {
// replyId: `${editComments.data?.replyId}`,
// creatorId: `${userId}`,
// createdAt: getRelativeTime(editComments.data?.createdAt),
// lastUpdatedAt: "2 hrs ago",
// comment: value,
// }
// updateReply((val as CommentSchema).threadId, (val as Reply)?.replyId, commentData);
if (threadSocket) {
// projectId, userId, comment, organization, threadId
const editComment = {
projectId,
userId,
comment: value,
organization,
threadId: selectedComment?.threadId,
commentId: (val as Reply).replyId ?? ""
}
threadSocket.emit("v1-Comment:add", editComment);
setIsEditable && setIsEditable(true);
setEditedThread && setEditedThread(false)
}
} catch {
}
}
}
// setValue("");
setIsEditComment(false);
} }
function handleDeleteAction() { const handleDeleteAction = async (replyID: any) => {
if (!projectId) return
setOpenOptions(false); setOpenOptions(false);
try {
// const deletedComment = await deleteCommentApi(projectId, selectedComment?.threadId, (val as Reply).replyId)
//
// if (deletedComment === "'Thread comment deleted Successfully'") {
// setMessages && setMessages(prev => prev.filter(message => message.replyId !== replyID));
// removeReply(val.creatorId, replyID)
// }
if (threadSocket && setMessages) {
// projectId, userId, commentId, organization, threadId
const deleteComment = {
projectId,
userId,
commentId: (val as Reply).replyId,
organization,
threadId: selectedComment?.threadId
}
setMessages(prev => prev.filter(message => message.replyId !== (val as Reply).replyId))
removeReply(selectedComment?.threadId, (val as Reply).replyId); // Remove listener after response
threadSocket.emit("v1-Comment:delete", deleteComment);
}
} catch {
}
} }
const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
requestAnimationFrame(() => {
if (textareaRef.current) {
const length = textareaRef.current.value.length;
textareaRef.current.setSelectionRange(length, length);
}
});
};
return ( return (
<> <>
{isEditing ? ( {isEditComment ? (
<div className="edit-container"> <div className="edit-container">
<div className="input-container"> <div className="input-container">
<textarea <textarea
@ -49,6 +186,7 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
style={{ resize: "none" }} style={{ resize: "none" }}
onFocus={handleFocus}
/> />
</div> </div>
<div className="actions-container"> <div className="actions-container">
@ -77,16 +215,16 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
<div className="message-container"> <div className="message-container">
<div <div
className="profile" className="profile"
style={{ background: getAvatarColor(i, UserName) }} style={{ background: getAvatarColor(i, userName) }}
> >
{UserName[0]} {userName?.charAt(0).toUpperCase() || "user"}
</div> </div>
<div className="content"> <div className="content">
<div className="user-details"> <div className="user-details">
<div className="user-name">{UserName}</div> <div className="user-name">{userName}</div>
<div className="time">{val.createdAt}</div> <div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div>
</div> </div>
{val.creatorId === currentUser && ( {(val as Reply).creatorId === userId && (
<div className="more-options"> <div className="more-options">
<button <button
className="more-options-button" className="more-options-button"
@ -100,30 +238,33 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
<div className="options-list"> <div className="options-list">
<button <button
className="option" className="option"
onClick={() => { onClick={(e) => {
e.preventDefault();
setMode && setMode("edit")
setOpenOptions(false); setOpenOptions(false);
setIsEditing(true); setEditedThread && setEditedThread(true);
setIsEditComment(true)
}} }}
> >
Edit Edit
</button> </button>
<button {!(isEditableThread) && <button
className="option" className="option"
onClick={() => { onClick={() => {
handleDeleteAction(); handleDeleteAction((val as Reply).replyId);
}} }}
> >
Delete Delete
</button> </button>}
</div> </div>
)} )}
</div> </div>
)} )}
<div className="message"> <div className="message">
{"reply" in val ? val.reply : val.comment} {"comment" in val ? val.comment : val.threadTitle}
</div> </div>
</div> </div>
</div> </div >
)} )}
</> </>
); );

View File

@ -3,36 +3,67 @@ import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
import Messages from "./Messages"; import Messages from "./Messages";
import { ExpandIcon } from "../../icons/SimulationIcons"; import { ExpandIcon } from "../../icons/SimulationIcons";
import { adjustHeight } from "./function/textAreaHeightAdjust"; 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 ThreadChat: React.FC = () => {
const { userId, organization } = getUserData();
const [openThreadOptions, setOpenThreadOptions] = useState(false); const [openThreadOptions, setOpenThreadOptions] = useState(false);
const [inputActive, setInputActive] = useState(false); const [inputActive, setInputActive] = useState(false);
const [value, setValue] = useState<string>(""); 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 textareaRef = useRef<HTMLTextAreaElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<Reply[]>([])
const { projectId } = useParams();
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const [selectedDiv, setSelectedDiv] = useState(true);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState({ x: 100, y: 100 }); 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 messages = [ useEffect(() => {
{ modeRef.current = mode;
replyId: "user 1", }, [mode]);
creatorId: "1",
createdAt: "2 hrs ago", useEffect(() => {
lastUpdatedAt: "2 hrs ago", if (comments.length > 0 && selectedComment) {
reply:
"reply testing reply content 1, reply testing reply content 1reply testing reply content 1",
}, const allMessages = comments
{ .flatMap((val: any) =>
replyId: "user 2", val?.threadId === selectedComment?.threadId ? val.comments : []
creatorId: "2", )
createdAt: "2 hrs ago", .map((c) => {
lastUpdatedAt: "2 hrs ago", return {
reply: "reply 2", 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(() => { useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current); if (textareaRef.current) adjustHeight(textareaRef.current);
@ -44,6 +75,19 @@ const ThreadChat: React.FC = () => {
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => { const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return; 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; const wrapper = wrapperRef.current;
if (!wrapper) return; if (!wrapper) return;
@ -58,18 +102,20 @@ const ThreadChat: React.FC = () => {
wrapper.setPointerCapture(event.pointerId); wrapper.setPointerCapture(event.pointerId);
}; };
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => { const updatePosition = (
if (!dragging) return; { clientX, clientY }: { clientX: number; clientY: number },
allowMove: boolean = true
) => {
if (!allowMove || !wrapperRef.current) return;
const container = document.getElementById("work-space-three-d-canvas"); const container = document.getElementById("work-space-three-d-canvas");
const wrapper = wrapperRef.current; const wrapper = wrapperRef.current;
if (!container || !wrapper) return; if (!container || !wrapper) return;
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
let newX = event.clientX - containerRect.left - dragOffset.x; let newX = clientX - containerRect.left - dragOffset.x;
let newY = event.clientY - containerRect.top - dragOffset.y; let newY = clientY - containerRect.top - dragOffset.y;
const maxX = containerRect.width - wrapper.offsetWidth; const maxX = containerRect.width - wrapper.offsetWidth;
const maxY = containerRect.height - wrapper.offsetHeight; const maxY = containerRect.height - wrapper.offsetHeight;
@ -80,6 +126,15 @@ const ThreadChat: React.FC = () => {
setPosition({ x: newX, y: newY }); 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>) => { const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return; if (!dragging) return;
setDragging(false); setDragging(false);
@ -87,12 +142,146 @@ const ThreadChat: React.FC = () => {
if (wrapper) wrapper.releasePointerCapture(event.pointerId); 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 ( return (
<div <div
ref={wrapperRef} ref={wrapperRef}
className="thread-chat-wrapper" className="thread-chat-wrapper"
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove} onPointerMove={(e) => handlePointerMove({ clientX: e.clientX, clientY: e.clientY })}
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
style={{ style={{
position: "absolute", position: "absolute",
@ -107,49 +296,94 @@ const ThreadChat: React.FC = () => {
<div className="header-wrapper"> <div className="header-wrapper">
<div className="header">Comment</div> <div className="header">Comment</div>
<div className="header-options"> <div className="header-options">
<button <div
className="options-button" className="options-button"
onClick={() => setOpenThreadOptions(!openThreadOptions)} style={{ cursor: "pointer" }}
onClick={(e) => {
e.preventDefault();
setOpenThreadOptions((prev) => !prev);
}}
> >
<KebabIcon /> <KebabIcon />
</button> </div>
{openThreadOptions && ( {openThreadOptions && (
<div className="options-list"> <div className="options-list">
<div className="options">Mark as Unread</div> <div className="options">Mark as Unread</div>
<div className="options">Mark as Resolved</div> <div className="options">Mark as Resolved</div>
<div className="options delete">Delete Thread</div> <div className="options delete" onClick={handleDeleteThread}>Delete Thread</div>
</div> </div>
)} )}
<button className="close-button"> <button className="close-button" onClick={() => {
setSelectedComment(null)
setCommentPositionState(null)
}}>
<CloseIcon /> <CloseIcon />
</button> </button>
</div> </div>
</div> </div>
<div className="messages-wrapper"> <div className="messages-wrapper" ref={messagesRef}>
{messages.map((val, i) => ( {selectedComment &&
<Messages val={val as any} i={i} key={val.replyId} /> <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>
<div className="send-message-wrapper"> <div className="send-message-wrapper">
<div className={`input-container ${inputActive ? "active" : ""}`}> <div className={`input-container ${inputActive ? "active" : ""}`}>
<textarea <textarea
placeholder="type something" placeholder={commentPositionState && selectedComment === null ? "Type Thread Title" : "type something"}
ref={textareaRef} ref={textareaRef}
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
onFocus={() => setInputActive(true)} 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)} onBlur={() => setInputActive(false)}
onFocus={() => setInputActive(true)}
style={{ resize: "none" }} style={{ resize: "none" }}
/> />
<div className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}> <div
className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}
onClick={(e) => {
if (commentPositionState && selectedComment === null) {
handleCreateThread(e);
} else {
setMode("create");
handleCreateComments(e);
}
setValue('')
}}
>
<ExpandIcon /> <ExpandIcon />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <ThreadSocketResponsesDev setMessages={setMessages} modeRef={modeRef} messages={messages} />
</div >
); );
}; };

View File

@ -0,0 +1,27 @@
type RelativeTimeFormatUnit = any;
export function getRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals: Record<RelativeTimeFormatUnit, number> = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
for (const key in intervals) {
const unit = key as RelativeTimeFormatUnit;
const diff = Math.floor(diffInSeconds / intervals[unit]);
if (diff >= 1) {
return rtf.format(-diff, unit);
}
}
return "just now";
}

View File

@ -1,18 +1,17 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useActiveTool } from "../../../store/builder/store"
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import { MathUtils, Vector3 } from "three"; import { MathUtils, Vector3 } from "three";
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
import CommentInstances from "./instances/commentInstances"; import CommentInstances from "./instances/commentInstances";
import { Sphere } from "@react-three/drei"; import { Sphere } from "@react-three/drei";
import { useActiveTool, useSelectedComment } from "../../../store/builder/store";
function CommentsGroup() { function CommentsGroup() {
const { gl, raycaster, camera, scene, pointer } = useThree(); const { gl, raycaster, camera, scene, pointer, size } = useThree();
const { activeTool } = useActiveTool(); const { activeTool } = useActiveTool();
const { addComment } = useCommentStore();
const [hoverPos, setHoverPos] = useState<Vector3 | null>(null); const [hoverPos, setHoverPos] = useState<Vector3 | null>(null);
const { setSelectedComment, setCommentPositionState, setPosition2Dstate } = useSelectedComment();
const userId = localStorage.getItem('userId') ?? '';
useEffect(() => { useEffect(() => {
const canvasElement = gl.domElement; const canvasElement = gl.domElement;
@ -64,7 +63,7 @@ function CommentsGroup() {
} }
}; };
const onMouseClick = () => { const onMouseClick = async () => {
if (drag) return; if (drag) return;
const intersects = raycaster const intersects = raycaster
@ -83,23 +82,16 @@ function CommentsGroup() {
!intersect.object.name.includes("SelectionGroupBoundingLine") && !intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper" intersect.object.type !== "GridHelper"
); );
console.log('intersects: ', intersects);
if (intersects.length > 0) { if (intersects.length > 0) {
const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z); const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z);
setSelectedComment(null);
setCommentPositionState({ position: position.toArray() })
const comment: CommentSchema = {
state: 'active',
commentId: MathUtils.generateUUID(),
creatorId: userId,
createdAt: new Date().toISOString(),
comment: '',
lastUpdatedAt: new Date().toISOString(),
position: position.toArray(),
rotation: [0, 0, 0],
replies: []
}
addComment(comment); position.project(camera);
const x = (position.x * 0.5 + 0.5) * size.width;
const y = (-(position.y * 0.5) + 0.5) * size.height;
setPosition2Dstate({ x, y })
setHoverPos(null); setHoverPos(null);
} }
} }
@ -124,7 +116,6 @@ function CommentsGroup() {
return ( return (
<> <>
<CommentInstances /> <CommentInstances />
{hoverPos && ( {hoverPos && (
<Sphere name={'commentHolder'} args={[0.1, 16, 16]} position={hoverPos}> <Sphere name={'commentHolder'} args={[0.1, 16, 16]} position={hoverPos}>
<meshStandardMaterial color="orange" /> <meshStandardMaterial color="orange" />

View File

@ -1,64 +1,90 @@
import { Html, TransformControls } from '@react-three/drei'; import { Html, TransformControls } from '@react-three/drei';
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads'; import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads';
import { useSelectedComment } from '../../../../../store/builder/store';
import { Group, Object3D, Vector2, Vector3 } from 'three';
import { useThree } from '@react-three/fiber';
function CommentInstance({ comment }: { comment: CommentSchema }) { function CommentInstance({ comment }: { comment: CommentSchema }) {
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const CommentRef = useRef(null); const CommentRef = useRef(null);
const [selectedComment, setSelectedComment] = useState<CommentSchema | null>(null); const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
const { selectedComment, setSelectedComment, setPosition2Dstate } = useSelectedComment()
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null); const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
const groupRef = useRef<Group>(null);
const { size, camera } = useThree();
// useEffect(() => {
// const handleKeyDown = (e: KeyboardEvent) => {
// const keyCombination = detectModifierKeys(e);
// if (!selectedComment) return;
// if (keyCombination === "G") {
// setTransformMode((prev) => (prev === "translate" ? null : "translate"));
// }
// if (keyCombination === "R") {
// setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
// }
// };
useEffect(() => { // window.addEventListener("keydown", handleKeyDown);
const handleKeyDown = (e: KeyboardEvent) => { // return () => window.removeEventListener("keydown", handleKeyDown);
const keyCombination = detectModifierKeys(e); // }, [selectedComment]);
if (!selectedComment) return;
if (keyCombination === "G") {
setTransformMode((prev) => (prev === "translate" ? null : "translate"));
}
if (keyCombination === "R") {
setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedComment]);
const commentClicked = () => { const commentClicked = () => {
console.log('hii');
setSelectedComment(comment); setSelectedComment(comment);
const position = new Vector3(comment.position[0], comment.position[1], comment.position[2])
position.project(camera);
const x = (position.x * 0.5 + 0.5) * size.width;
const y = (-(position.y * 0.5) + 0.2) * size.height;
setPosition2Dstate({ x, y })
if (groupRef.current) {
setSelectedObject(groupRef.current);
}
} }
if (comment.state === 'inactive' || isPlaying) return null; useEffect(() => {
if (!selectedComment || selectedComment.threadId !== comment.threadId) {
setSelectedObject(null)
}
}, [selectedComment])
if (comment.state === 'inactive' || isPlaying) return null;
return ( return (
<> <>
<Html <group ref={groupRef} position={comment.position} rotation={comment.rotation}>
ref={CommentRef} <Html
zIndexRange={[1, 0]} ref={CommentRef}
prepend zIndexRange={[1, 0]}
sprite prepend
center sprite
position={comment.position} center
rotation={comment.rotation} // position={comment.position}
className='comments-main-wrapper' // rotation={comment.rotation}
> className='comments-main-wrapper'
<CommentThreads commentClicked={commentClicked} /> >
</Html> <CommentThreads commentClicked={commentClicked} comment={comment} />
{CommentRef.current && transformMode && ( </Html>
</group>
{/* {selectedObject && transformMode && (
<TransformControls <TransformControls
object={CommentRef.current} object={selectedObject}
mode={transformMode} mode={transformMode}
onMouseUp={(e) => { onMouseUp={(e) => {
console.log("sad");
}} }}
/> />
)} )} */}
</> </>
) )
} }
export default CommentInstance; export default CommentInstance;

View File

@ -1,18 +1,56 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import CommentInstance from './commentInstance/commentInstance' import CommentInstance from './commentInstance/commentInstance'
import { useCommentStore } from '../../../../store/collaboration/useCommentStore' import { useCommentStore } from '../../../../store/collaboration/useCommentStore'
import { getAllThreads } from '../../../../services/factoryBuilder/comments/getAllThreads';
import { useParams } from 'react-router-dom';
import { getUserData } from '../../../../functions/getUserData';
import { getRelativeTime } from '../../../../components/ui/collaboration/function/getRelativeTime';
function CommentInstances() { function CommentInstances() {
const { comments } = useCommentStore(); const { comments, setComments } = useCommentStore();
const { projectId } = useParams();
const { userId } = getUserData()
const getThreads = async () => {
if (!projectId) return;
try {
const getComments = await getAllThreads(projectId);
const formattedThreads = Array.isArray(getComments.data)
? getComments.data.map((thread: any) => ({
...thread,
comments: Array.isArray(thread.comments)
? thread.comments.map((val: any) => ({
replyId: val._id ?? "",
creatorId: userId,
createdAt: getRelativeTime(val.createdAt),
lastUpdatedAt: "1 hr ago",
comment: val.comment,
_id: val._id ?? "",
}))
: [],
}))
: [];
// console.log('formattedThreads: ', formattedThreads);
setComments(formattedThreads);
} catch (err) {
// console.error("Failed to fetch threads:", err);
}
}
useEffect(() => { useEffect(() => {
// console.log('comments: ', comments); getThreads();
}, [comments]) }, []);
useEffect(() => {
// console.log("comments", comments);
}, [comments])
return ( return (
<> <>
{comments.map((comment: CommentSchema) => ( {comments?.map((comment: CommentSchema) => (
<React.Fragment key={comment.commentId}> <React.Fragment key={comment.threadId}>
<CommentInstance comment={comment} /> <CommentInstance comment={comment} />
</React.Fragment> </React.Fragment>
))} ))}

View File

@ -0,0 +1,168 @@
import React, { useEffect } from 'react';
import { useSelectedComment, useSocketStore } from '../../../store/builder/store';
import { useCommentStore } from '../../../store/collaboration/useCommentStore';
import { getUserData } from '../../../functions/getUserData';
import { getRelativeTime } from '../../../components/ui/collaboration/function/getRelativeTime';
interface ThreadSocketProps {
setMessages: React.Dispatch<React.SetStateAction<Reply[]>>;
// mode: 'create' | 'edit' | null
modeRef: React.RefObject<'create' | 'edit' | null>;
messages: Reply[]
}
const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSocketProps) => {
const { threadSocket } = useSocketStore();
const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState } = useSelectedComment();
const { comments, addComment, addReply, updateComment, updateReply } = useCommentStore();
const { userId } = getUserData();
useEffect(() => {
if (!threadSocket) return;
// --- Add Comment Handler ---
// const handleAddComment = (data: any) => {
//
//
// const commentData = {
// replyId: data.data?._id,
// creatorId: data.data?.userId,
// createdAt: getRelativeTime(data.data?.createdAt),
// lastUpdatedAt: '2 hrs ago',
// comment: data.data.comment,
// };
// //
//
// if (mode == "create") {
// addReply(selectedComment?.threadId, commentData);
//
// setMessages(prevMessages => [...prevMessages, commentData]);
// } else if (mode == "edit") {
// updateReply(selectedComment?.threadId, data.data?._id, commentData);
// setMessages((prev) =>
// prev.map((message) => {
// // 👈 log each message
// return (message.replyId || message._id) === data.data?._id
// ? { ...message, comment: data.data?.comment }
// : message;
// })
// );
//
// } else {
//
// }
// threadSocket.off('v1-Comment:response:add', handleAddComment);
// };
// threadSocket.on('v1-Comment:response:add', handleAddComment);
const handleAddComment = (data: any) => {
// console.log('Add: ', data);
const commentData = {
replyId: data.data?._id,
creatorId: data.data?.userId,
createdAt: getRelativeTime(data.data?.createdAt),
lastUpdatedAt: "2 hrs ago",
comment: data.data.comment,
};
if (modeRef.current === "create") {
addReply(selectedComment?.threadId, commentData);
setMessages((prevMessages) => [...prevMessages, commentData]);
} else if (modeRef.current === "edit") {
updateReply(selectedComment?.threadId, data.data?._id, commentData);
setMessages((prev) =>
prev.map((message) => {
// 👈 log each message
return message.replyId === data.data?._id
? { ...message, comment: data.data?.comment }
: message;
})
);
// console.log('data.data?.comment: ', data.data?.comment);
} else {
}
threadSocket.off("v1-Comment:response:add", handleAddComment);
};
threadSocket.on('v1-Comment:response:add', handleAddComment);
// --- Delete Comment Handler ---
const handleDeleteComment = (data: any) => {
// console.log('delete: ', data);
threadSocket.off('v1-Comment:response:delete', handleDeleteComment);
};
threadSocket.on('v1-Comment:response:delete', handleDeleteComment);
// --- Create Thread Handler ---
const handleCreateThread = (data: any) => {
// console.log('createThread: ', data);
const comment: CommentSchema = {
state: 'active',
threadId: data.data?._id,
creatorId: userId || data.data?.userId,
createdAt: data.data?.createdAt,
threadTitle: data.data?.threadTitle,
lastUpdatedAt: new Date().toISOString(),
position: commentPositionState.position,
rotation: [0, 0, 0],
comments: [],
};
setSelectedComment(comment)
addComment(comment);
setCommentPositionState(null);
// setSelectedComment(null);
threadSocket.off('v1-thread:response:create', handleCreateThread);
};
threadSocket.on('v1-thread:response:create', handleCreateThread);
// --- Delete Thread Handler ---
const handleDeleteThread = (data: any) => {
threadSocket.off('v1-thread:response:delete', handleDeleteThread);
};
threadSocket.on('v1-thread:response:delete', handleDeleteThread);
const handleEditThread = (data: any) => {
const editedThread: CommentSchema = {
state: 'active',
threadId: data.data?._id,
creatorId: userId,
createdAt: data.data?.createdAt,
threadTitle: data.data?.threadTitle,
lastUpdatedAt: new Date().toISOString(),
position: data.data.position,
rotation: [0, 0, 0],
comments: data.data.comments,
};
// console.log('data.data?._id: ', data.data?._id);
updateComment(data.data?._id, editedThread);
setSelectedComment(editedThread)
// setSelectedComment(null);
};
threadSocket.on('v1-thread:response:updateTitle', handleEditThread);
// Cleanup on unmount
return () => {
threadSocket.off('v1-Comment:response:add', handleAddComment);
threadSocket.off('v1-Comment:response:delete', handleDeleteComment);
threadSocket.off('v1-thread:response:create', handleCreateThread);
threadSocket.off('v1-thread:response:delete', handleDeleteThread);
threadSocket.off('v1-thread:response:updateTitle', handleEditThread);
};
}, [threadSocket, selectedComment, commentPositionState, userId, setMessages, addReply, addComment, setSelectedComment, setCommentPositionState, updateComment, comments, modeRef.current, messages]);
return null;
};
export default ThreadSocketResponsesDev;

View File

@ -6,8 +6,12 @@ import {
useUserName, useUserName,
useWallItems, useWallItems,
useSaveVersion, useSaveVersion,
useViewSceneStore,
useProjectName, useProjectName,
useRenameModeStore,
useSelectedFloorItem,
useZones, useZones,
useSelectedComment,
} from "../store/builder/store"; } from "../store/builder/store";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useSelectedUserStore } from "../store/collaboration/useCollabStore"; import { useSelectedUserStore } from "../store/collaboration/useCollabStore";
@ -26,6 +30,7 @@ import { SceneProvider } from "../modules/scene/sceneContext";
import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/getVersionHistoryApi"; import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/getVersionHistoryApi";
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore"; import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
import { VersionProvider } from "../modules/builder/version/versionContext"; import { VersionProvider } from "../modules/builder/version/versionContext";
import ThreadChat from "../components/ui/collaboration/ThreadChat";
const Project: React.FC = () => { const Project: React.FC = () => {
let navigate = useNavigate(); let navigate = useNavigate();
@ -44,6 +49,7 @@ const Project: React.FC = () => {
const { selectedUser } = useSelectedUserStore(); const { selectedUser } = useSelectedUserStore();
const { isLogListVisible } = useLogger(); const { isLogListVisible } = useLogger();
const { setVersions } = useVersionHistoryStore(); const { setVersions } = useVersionHistoryStore();
const { selectedComment, setSelectedComment, commentPositionState } = useSelectedComment();
useEffect(() => { useEffect(() => {
if (!email || !userId) { if (!email || !userId) {
@ -124,6 +130,7 @@ const Project: React.FC = () => {
<LogList /> <LogList />
</RenderOverlay> </RenderOverlay>
)} )}
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat />}
</div> </div>
); );
}; };

View File

@ -0,0 +1,46 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const addCommentsApi = async (
projectId: any,
comment: string,
threadId?: string,
commentId?: string
) => {
console.log(
" projectId, comments, threadId: ",
projectId,
comment,
threadId,
commentId
);
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/v1/Thread/addComment`,
{
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectId, comment, threadId, commentId }),
}
);
console.log('response: ', response);
if (!response.ok) {
throw new Error("Failed to add project");
}
const result = await response.json();
console.log("result: ", result);
return result;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred");
}
}
};

View File

@ -0,0 +1,34 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const createThreadApi = async (
projectId: any,
state: string,
position: any,
rotation: any,
threadTitle: any
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/v1/upsetThread`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectId, state, position, rotation, threadTitle }),
});
if (!response.ok) {
throw new Error("Failed to add project");
}
const result = await response.json();
console.log('result: ', result);
return result;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred");
}
}
};

View File

@ -0,0 +1,43 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
export const deleteCommentApi = async (
projectId: string,
threadId: string,
commentId: string
) => {
const body: any = {
projectId,
threadId,
commentId,
};
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/v1/Thread/deleteComment`,
{
method: "PATCH",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify(body),
}
);
if (!response.ok) {
throw new Error("Failed to clearPanel in the zone");
}
const result = await response.json();
return result;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred");
}
}
};

View File

@ -0,0 +1,35 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
export const deleteThreadApi = async (projectId: string, threadId: string) => {
const body: any = {
projectId,
threadId,
};
try {
const response = await fetch(`${url_Backend_dwinzo}/api/v1/Thread/delete`, {
method: "PATCH",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error("Failed to clearPanel in the zone");
}
const result = await response.json();
return result;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred");
}
}
};

View File

@ -0,0 +1,43 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
export const editThreadTitleApi = async (
projectId: string,
threadId: string,
threadTitle: string
) => {
const body: any = {
projectId,
threadId,
threadTitle,
};
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/v1/Thread/updateTitle`,
{
method: "PATCH",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify(body),
}
);
if (!response.ok) {
throw new Error("Failed to clearPanel in the zone");
}
const result = await response.json();
return result;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred");
}
}
};

View File

@ -0,0 +1,34 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const getAllThreads = async (projectId: string) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/v1/Threads/${projectId}`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (!response.ok) {
throw new Error("Failed to get assets");
}
const result = await response.json();
// console.log('result: ', result);
return result;
} catch (error) {
echo.error("Failed to get floor asset");
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred");
}
}
};

View File

@ -27,7 +27,6 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
} }
); );
const dashBoardSocket = io( const dashBoardSocket = io(
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/dashboard`, `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/dashboard`,
{ {
@ -42,8 +41,21 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
auth: { token }, auth: { token },
} }
); );
const threadSocket = io(
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`,
{
reconnection: true,
auth: { token },
}
);
set({ socket, visualizationSocket, dashBoardSocket, projectSocket }); set({
socket,
visualizationSocket,
dashBoardSocket,
projectSocket,
threadSocket,
});
}, },
disconnectSocket: () => { disconnectSocket: () => {
set((state: any) => { set((state: any) => {
@ -51,6 +63,7 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
state.visualizationSocket?.disconnect(); state.visualizationSocket?.disconnect();
state.dashBoardSocket?.disconnect(); state.dashBoardSocket?.disconnect();
state.projectSocket?.disconnect(); state.projectSocket?.disconnect();
state.threadSocket?.disconnect();
return { socket: null }; return { socket: null };
}); });
}, },
@ -657,10 +670,10 @@ export const useViewSceneStore = create<ViewSceneState>((set) => ({
setViewSceneLabels: (value) => { setViewSceneLabels: (value) => {
set((state) => { set((state) => {
const newValue = const newValue =
typeof value === 'function' ? value(state.viewSceneLabels) : value; typeof value === "function" ? value(state.viewSceneLabels) : value;
// Store in localStorage manually // Store in localStorage manually
localStorage.setItem('viewSceneLabels', JSON.stringify(newValue)); localStorage.setItem("viewSceneLabels", JSON.stringify(newValue));
return { viewSceneLabels: newValue }; return { viewSceneLabels: newValue };
}); });
@ -668,8 +681,8 @@ export const useViewSceneStore = create<ViewSceneState>((set) => ({
})); }));
function getInitialViewSceneLabels(): boolean { function getInitialViewSceneLabels(): boolean {
if (typeof window === 'undefined') return false; // SSR safety if (typeof window === "undefined") return false; // SSR safety
const saved = localStorage.getItem('viewSceneLabels'); const saved = localStorage.getItem("viewSceneLabels");
return saved ? JSON.parse(saved) : false; return saved ? JSON.parse(saved) : false;
} }
export interface CompareProduct { export interface CompareProduct {
@ -691,7 +704,7 @@ export interface CompareProduct {
machineIdleTime: number; machineIdleTime: number;
machineActiveTime: number; machineActiveTime: number;
throughputData: number; throughputData: number;
} };
} }
export const useCompareProductDataStore = create<{ export const useCompareProductDataStore = create<{
@ -700,4 +713,13 @@ export const useCompareProductDataStore = create<{
}>((set) => ({ }>((set) => ({
compareProductsData: [], compareProductsData: [],
setCompareProductsData: (x) => set({ compareProductsData: x }), setCompareProductsData: (x) => set({ compareProductsData: x }),
})); }));
export const useSelectedComment = create<any>((set: any) => ({
selectedComment: null,
setSelectedComment: (x: any) => set({ selectedComment: x }),
position2Dstate: {},
setPosition2Dstate: (x: any) => set({ position2Dstate: x }),
commentPositionState: null,
setCommentPositionState: (x: any) => set({ commentPositionState: x }),
}));

View File

@ -1,92 +1,97 @@
import { create } from 'zustand'; import { create } from "zustand";
import { immer } from 'zustand/middleware/immer'; import { immer } from "zustand/middleware/immer";
interface CommentStore { interface CommentStore {
comments: CommentsSchema; comments: CommentsSchema;
// Comment operations // Comment operations
addComment: (comment: CommentSchema) => void; addComment: (comment: CommentSchema) => void;
setComments: (comments: CommentsSchema) => void; setComments: (comments: CommentsSchema) => void;
updateComment: (commentId: string, updates: Partial<CommentSchema>) => void; updateComment: (threadId: string, updates: Partial<CommentSchema>) => void;
removeComment: (commentId: string) => void; removeComment: (threadId: string) => void;
// Reply operations // Reply operations
addReply: (commentId: string, reply: Reply) => void; addReply: (threadId: string, reply: Reply) => void;
updateReply: (commentId: string, replyId: string, updates: Partial<Reply>) => void; updateReply: (
removeReply: (commentId: string, replyId: string) => void; threadId: string,
replyId: string,
updates: Partial<Reply>
) => void;
removeReply: (threadId: string, _id: string) => void;
// Getters // Getters
getCommentById: (commentId: string) => CommentSchema | undefined; getCommentById: (threadId: string) => CommentSchema | undefined;
} }
export const useCommentStore = create<CommentStore>()( export const useCommentStore = create<CommentStore>()(
immer((set, get) => ({ immer((set, get) => ({
comments: [], comments: [],
// Comment operations // Comment operations
addComment: (comment) => { addComment: (comment) => {
set((state) => { set((state) => {
if (!state.comments.find(c => c.commentId === comment.commentId)) { if (!state.comments.find((c) => c.threadId === comment.threadId)) {
state.comments.push(JSON.parse(JSON.stringify(comment))); state.comments.push(JSON.parse(JSON.stringify(comment)));
} }
}); });
}, },
setComments: (comments) => { setComments: (comments) => {
set((state) => { set((state) => {
state.comments = comments; state.comments = comments;
}); });
}, },
updateComment: (commentId, updates) => { updateComment: (threadId, updates) => {
set((state) => { console.log("threadId:updater ", threadId);
const comment = state.comments.find(c => c.commentId === commentId); set((state) => {
if (comment) { const comment = state.comments.find((c) => c.threadId === threadId);
Object.assign(comment, updates); if (comment) {
} Object.assign(comment, updates);
}); }
}, });
},
removeComment: (commentId) => { removeComment: (threadId) => {
set((state) => { set((state) => {
state.comments = state.comments.filter(c => c.commentId !== commentId); state.comments = state.comments.filter((c) => c.threadId !== threadId);
}); });
}, },
// Reply operations // Reply operations
addReply: (commentId, reply) => { addReply: (threadId, comment) => {
set((state) => { set((state) => {
const comment = state.comments.find(c => c.commentId === commentId); const reply = state.comments.find((c) => c.threadId === threadId);
if (comment) { if (reply) {
comment.replies.push(reply); reply.comments.push(comment);
} }
}); });
}, },
updateReply: (commentId, replyId, updates) => { updateReply: (threadId, replyId, updates) => {
set((state) => { set((state) => {
const comment = state.comments.find(c => c.commentId === commentId); const reply = state.comments.find((c) => c.threadId === threadId);
if (comment) { if (reply) {
const reply = comment.replies.find(r => r.replyId === replyId); const comment = reply.comments.find((r) => r.replyId === replyId);
if (reply) { if (comment) {
Object.assign(reply, updates); Object.assign(comment, updates);
} }
} }
}); });
}, },
removeReply: (commentId, replyId) => { removeReply: (threadId, _id) => {
set((state) => { set((state) => {
const comment = state.comments.find(c => c.commentId === commentId); const comment = state.comments.find((c) => c.threadId === threadId);
if (comment) { if (comment) {
comment.replies = comment.replies.filter(r => r.replyId !== replyId); comment.comments = comment.comments.filter((r) => r.replyId !== _id);
} }
}); });
}, },
// Getter // Getter
getCommentById: (commentId) => { getCommentById: (threadId) => {
return get().comments.find(c => c.commentId === commentId); return get().comments.find((c) => c.threadId === threadId);
}, },
})) }))
); );

View File

@ -56,16 +56,23 @@
.message { .message {
margin-top: 10px; margin-top: 10px;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
text-align: start;
} }
.replies { .replies,
.comments {
margin-top: 4px; margin-top: 4px;
font-size: var(--font-size-small); font-size: var(--font-size-small);
color: var(--input-text-color); color: var(--input-text-color);
} }
.header, .header,
.message, .message,
.replies { .replies,
.comments {
display: none; display: none;
opacity: 0; opacity: 0;
} }
@ -84,9 +91,15 @@
.users-commented { .users-commented {
padding: 12px; padding: 12px;
} }
.message {
display: -webkit-box !important;
opacity: 1 !important;
margin-bottom: 4px;
padding: 0;
}
.header, .header,
.message, .replies,
.replies { .comments {
display: flex !important; display: flex !important;
opacity: 1 !important; opacity: 1 !important;
} }
@ -158,11 +171,13 @@
.messages-wrapper { .messages-wrapper {
padding: 12px; padding: 12px;
padding-top: 0; padding-top: 0;
max-height: 50vh;
overflow-y: auto;
.edit-container { .edit-container {
.input-container { .input-container {
textarea{ textarea {
background: var(--background-color); background: var(--background-color);
&:focus{ &:focus {
outline-color: var(--border-color-accent); outline-color: var(--border-color-accent);
} }
} }
@ -195,7 +210,7 @@
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 12px;
margin-top: 8px; margin-top: 8px;
&:first-child{ &:first-child {
margin: 0; margin: 0;
} }
.profile { .profile {
@ -231,7 +246,7 @@
height: 18px; height: 18px;
width: 18px; width: 18px;
border-radius: #{$border-radius-small}; border-radius: #{$border-radius-small};
&:hover{ &:hover {
background: var(--background-color-solid); background: var(--background-color-solid);
} }
} }
@ -249,14 +264,14 @@
border-radius: #{$border-radius-medium}; border-radius: #{$border-radius-medium};
padding: 2px 6px; padding: 2px 6px;
text-align: start; text-align: start;
&:hover{ &:hover {
background: var(--background-color-accent); background: var(--background-color-accent);
color: var(--text-button-color); color: var(--text-button-color);
} }
} }
} }
} }
.message{ .message {
margin-top: 6px; margin-top: 6px;
} }
} }
@ -284,7 +299,9 @@
bottom: 2px; bottom: 2px;
@include flex-center; @include flex-center;
padding: 2px; padding: 2px;
cursor: pointer;
svg { svg {
pointer-events: none;
rotate: 45deg; rotate: 45deg;
} }
} }

View File

@ -1,13 +1,13 @@
interface CommentSchema { interface CommentSchema {
state: "active" | "inactive"; state: "active" | "inactive";
commentId: string; threadId: string;
creatorId: string; creatorId: string;
createdAt: string; createdAt: string;
comment: string; threadTitle: string;
lastUpdatedAt: string; lastUpdatedAt: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
replies: Reply[]; comments: Reply[];
} }
interface Reply { interface Reply {
@ -15,7 +15,7 @@ interface Reply {
creatorId: string; creatorId: string;
createdAt: string; createdAt: string;
lastUpdatedAt: string; lastUpdatedAt: string;
reply: string; comment: string;
} }
type CommentsSchema = CommentSchema[]; type CommentsSchema = CommentSchema[];

View File

@ -7,6 +7,7 @@ import useVersionHistoryVisibleStore, {
useAddAction, useAddAction,
useRenameModeStore, useRenameModeStore,
useSaveVersion, useSaveVersion,
useSelectedComment,
useSelectedFloorItem, useSelectedFloorItem,
useSelectedWallItem, useSelectedWallItem,
useShortcutStore, useShortcutStore,
@ -47,7 +48,8 @@ const KeyPressListener: React.FC = () => {
const { selectedFloorItem } = useSelectedFloorItem(); const { selectedFloorItem } = useSelectedFloorItem();
const { setCreateNewVersion } = useVersionHistoryStore(); const { setCreateNewVersion } = useVersionHistoryStore();
const { setVersionHistoryVisible } = useVersionHistoryVisibleStore(); const { setVersionHistoryVisible } = useVersionHistoryVisibleStore();
const { setSelectedComment } = useSelectedComment();
const isTextInput = (element: Element | null): boolean => const isTextInput = (element: Element | null): boolean =>
element instanceof HTMLInputElement || element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement || element instanceof HTMLTextAreaElement ||
@ -193,6 +195,7 @@ const KeyPressListener: React.FC = () => {
clearComparisonProduct(); clearComparisonProduct();
setIsLogListVisible(false); setIsLogListVisible(false);
setIsRenameMode(false); setIsRenameMode(false);
setSelectedComment(null);
} }
if ( if (