threads api refactor

This commit is contained in:
2025-09-10 11:18:47 +05:30
parent aace9be40c
commit 060d7a7d82
16 changed files with 1076 additions and 1059 deletions

View File

@@ -26,7 +26,7 @@ import RegularDropDown from "../../ui/inputs/RegularDropDown";
import RenameTooltip from "../../ui/features/RenameTooltip"; import RenameTooltip from "../../ui/features/RenameTooltip";
import VersionSaved from "../sidebarRight/versionHisory/VersionSaved"; import VersionSaved from "../sidebarRight/versionHisory/VersionSaved";
import Footer from "../../footer/Footer"; import Footer from "../../footer/Footer";
import ThreadChat from "../../ui/collaboration/ThreadChat"; import ThreadChat from "../../ui/collaboration/threads/ThreadChat";
import Scene from "../../../modules/scene/scene"; import Scene from "../../../modules/scene/scene";
import { getUserData } from "../../../functions/getUserData"; import { getUserData } from "../../../functions/getUserData";
@@ -179,7 +179,9 @@ function MainScene() {
{isPlaying && activeModule === "simulation" && loadingProgress === 0 && <SimulationPlayer />} {isPlaying && activeModule === "simulation" && loadingProgress === 0 && <SimulationPlayer />}
{isPlaying && activeModule !== "simulation" && <ControlsPlayer />} {isPlaying && activeModule !== "simulation" && <ControlsPlayer />}
{isRenameMode && (selectedFloorAsset?.userData.modelName || selectedAssets.length === 1) && <RenameTooltip name={selectedFloorAsset?.userData.modelName || selectedAssets[0].userData.modelName} onSubmit={handleObjectRename} />} {isRenameMode && (selectedFloorAsset?.userData.modelName || selectedAssets.length === 1) && (
<RenameTooltip name={selectedFloorAsset?.userData.modelName || selectedAssets[0].userData.modelName} onSubmit={handleObjectRename} />
)}
{/* remove this later */} {/* remove this later */}
{activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />} {activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />}
</> </>

View File

@@ -1,115 +0,0 @@
import React, { useEffect, useState } from "react";
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 {
commentClicked: () => void;
comment?: CommentSchema
}
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, comment }) => {
const [expand, setExpand] = useState(false);
const commentsedUsers = [{ creatorId: "1" }];
const { userName } = getUserData();
const CommentDetails = {
state: "active",
commentId: "c-1",
creatorId: "12",
createdAt: "2 hours ago",
comment: "Thread check",
lastUpdatedAt: "string",
comments: [
{
replyId: "string",
creatorId: "string",
createdAt: "string",
lastUpdatedAt: "string",
comment: "string",
},
{
replyId: "string",
creatorId: "string",
createdAt: "string",
lastUpdatedAt: "string",
comment: "string",
},
],
};
function getUsername(userId: string) {
const UserName = userName?.charAt(0).toUpperCase() || "user";
return UserName;
}
function getDetails(type?: "clicked") {
if (type === "clicked") {
setExpand(true);
commentClicked();
} else {
setExpand((prev) => !prev);
}
}
return (
<div className="comments-threads-wrapper">
<button
onPointerEnter={() => getDetails()}
onPointerLeave={() => getDetails()}
onClick={() => getDetails("clicked")}
className={`comments-threads-container ${expand ? "open" : "closed"
} unread`}
>
<div className="users-commented">
{commentsedUsers.map((val, i) => (
<div
className="users"
key={val.creatorId}
style={{
background: getAvatarColor(i, getUsername(val.creatorId)),
}}
>
{getUsername(val.creatorId)[0]}
</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 className={`last-comment-details ${expand ? "expand" : ""}`}>
<div className="header">
<div className="user-name">
{userName}
{/* {getUsername(CommentDetails.creatorId)} */}
</div>
<div className="time">{comment?.createdAt && getRelativeTime(comment.createdAt)}</div>
</div>
<div className="message">{comment?.threadTitle}</div>
{comment && comment?.comments.length > 0 && (
<div className="comments">
{comment && comment?.comments.length}{" "}
{comment && comment?.comments.length === 1 ? "comment" : "replies"}
</div>
)}
</div>
</button>
</div>
);
};
export default CommentThreads;

View File

@@ -0,0 +1,68 @@
import React, { useState } from "react";
import { getAvatarColor } from "../../../../modules/collaboration/functions/getAvatarColor";
import { getUserData } from "../../../../functions/getUserData";
import { getRelativeTime } from "../function/getRelativeTime";
interface CommentThreadsProps {
commentClicked: () => void;
comment?: CommentSchema;
}
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, comment }) => {
const [expand, setExpand] = useState(false);
const commentsedUsers = [{ creatorId: "1" }];
const { userName } = getUserData();
function getUsername(userId: string) {
const UserName = userName?.charAt(0).toUpperCase() || "user";
return UserName;
}
function getDetails(type?: "clicked") {
if (type === "clicked") {
setExpand(true);
commentClicked();
} else {
setExpand((prev) => !prev);
}
}
return (
<div className="comments-threads-wrapper">
<button
onPointerEnter={() => getDetails()}
onPointerLeave={() => getDetails()}
onClick={() => getDetails("clicked")}
className={`comments-threads-container ${expand ? "open" : "closed"} unread`}
>
<div className="users-commented">
{commentsedUsers.map((val, i) => (
<div
className="users"
key={val.creatorId}
style={{
background: getAvatarColor(i, getUsername(val.creatorId)),
}}
>
{getUsername(val.creatorId)[0]}
</div>
))}
</div>
<div className={`last-comment-details ${expand ? "expand" : ""}`}>
<div className="header">
<div className="user-name">{userName}</div>
<div className="time">{comment?.createdAt && getRelativeTime(comment.createdAt)}</div>
</div>
<div className="message">{comment?.threadTitle}</div>
{comment && comment?.comments.length > 0 && (
<div className="comments">
{comment?.comments.length} {comment?.comments.length === 1 ? "comment" : "replies"}
</div>
)}
</div>
</button>
</div>
);
};
export default CommentThreads;

View File

@@ -1,254 +1,259 @@
import React, { useEffect, useRef, useState } from "react"; 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 { getUserData } from "../../../../functions/getUserData";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useCommentStore } from "../../../store/collaboration/useCommentStore"; import { useSelectedComment, useSocketStore } from "../../../../store/builder/store";
import { useSelectedComment, useSocketStore } from "../../../store/builder/store"; import { getRelativeTime } from "../function/getRelativeTime";
import { getRelativeTime } from "./function/getRelativeTime"; import { editThreadTitleApi } from "../../../../services/factoryBuilder/comments/editThreadTitleApi";
import { editThreadTitleApi } from "../../../services/factoryBuilder/comments/editThreadTitleApi"; import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { deleteCommentApi } from "../../../../services/factoryBuilder/comments/deleteCommentApi";
// import { deleteCommentApi } from "../../../services/factoryBuilder/comments/deleteCommentApi"; import { addCommentsApi } from "../../../../services/factoryBuilder/comments/addCommentApi";
// import { addCommentsApi } from "../../../services/factoryBuilder/comments/addCommentsApi"; import { editCommentsApi } from "../../../../services/factoryBuilder/comments/editCommentApi";
interface MessageProps { interface MessageProps {
val: Reply | CommentSchema; val: Reply | CommentSchema;
i: number; i: number;
setMessages?: React.Dispatch<React.SetStateAction<Reply[]>>; setMessages?: React.Dispatch<React.SetStateAction<Reply[]>>;
setIsEditable?: React.Dispatch<React.SetStateAction<boolean>>; setIsEditable?: React.Dispatch<React.SetStateAction<boolean>>;
setEditedThread?: React.Dispatch<React.SetStateAction<boolean>>; setEditedThread?: React.Dispatch<React.SetStateAction<boolean>>;
setMode?: React.Dispatch<React.SetStateAction<"create" | "edit" | null>>; setMode?: React.Dispatch<React.SetStateAction<"create" | "edit" | null>>;
isEditable?: boolean; isEditable?: boolean;
isEditableThread?: boolean; isEditableThread?: boolean;
editedThread?: boolean; editedThread?: boolean;
mode?: "create" | "edit" | null; mode?: "create" | "edit" | null;
} }
const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEditable, setEditedThread, editedThread, isEditableThread, setMode }) => { const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEditable, setEditedThread, editedThread, isEditableThread, setMode }) => {
const { updateComment, removeReply, updateReply } = useCommentStore(); const [openOptions, setOpenOptions] = useState(false);
const [openOptions, setOpenOptions] = useState(false); const { projectId } = useParams();
const { projectId } = useParams(); const { threadSocket } = useSocketStore();
const { threadSocket } = useSocketStore(); const { userName, userId, organization } = getUserData();
const { userName, userId, organization } = getUserData(); const [isEditComment, setIsEditComment] = useState(false);
const [isEditComment, setIsEditComment] = useState(false); const { selectedComment, setCommentPositionState } = useSelectedComment();
const { selectedComment, setCommentPositionState } = useSelectedComment(); const { versionStore, commentStore } = useSceneContext();
const { versionStore } = useSceneContext(); const { updateComment, removeReply, updateReply } = commentStore();
const { selectedVersion } = versionStore(); const { selectedVersion } = versionStore();
const [value, setValue] = useState<string>("comment" in val ? val.comment : val.threadTitle); const [value, setValue] = useState<string>("comment" in val ? val.comment : val.threadTitle);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => { useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current); if (textareaRef.current) adjustHeight(textareaRef.current);
}, [value]); }, [value]);
function handleCancelAction() { function handleCancelAction() {
setCommentPositionState(null); setCommentPositionState(null);
if (setIsEditable) setIsEditable(true); if (setIsEditable) setIsEditable(true);
setIsEditComment(false); setIsEditComment(false);
} }
const handleSaveAction = async () => { const handleSaveAction = async () => {
if (!projectId || !selectedVersion) return; if (!projectId || !selectedVersion || !setIsEditable || !setEditedThread) return;
if (isEditableThread && editedThread) { if (isEditableThread && editedThread) {
try { if (!threadSocket?.active) {
if (!threadSocket?.active) { // API
const editThreadTitle = await editThreadTitleApi(projectId, (val as CommentSchema).threadId, value, selectedVersion?.versionId || "");
if (editThreadTitle.message == "ThreadTitle updated Successfully") { editThreadTitleApi(projectId, (val as CommentSchema).threadId, value, selectedVersion?.versionId || "").then((editThreadTitle) => {
const editedThread: CommentSchema = { if (editThreadTitle.message == "ThreadTitle updated Successfully") {
state: "active", const editedThread: CommentSchema = {
threadId: editThreadTitle.data.replyId, state: "active",
creatorId: userId, threadId: editThreadTitle.data.replyId,
createdAt: getRelativeTime(editThreadTitle.data.createdAt), creatorId: userId,
threadTitle: value, createdAt: getRelativeTime(editThreadTitle.data.createdAt),
lastUpdatedAt: new Date().toISOString(), threadTitle: value,
position: editThreadTitle.data.position, lastUpdatedAt: new Date().toISOString(),
rotation: [0, 0, 0], position: editThreadTitle.data.position,
comments: [], rotation: [0, 0, 0],
}; comments: [],
updateComment((val as CommentSchema).threadId, editedThread); };
} updateComment((val as CommentSchema).threadId, editedThread);
} else { }
const threadEdit = { });
projectId, } else {
userId, // SOCKET
threadTitle: value,
organization, const threadEdit = {
threadId: (val as CommentSchema).threadId || selectedComment.threadId, projectId,
versionId: selectedVersion?.versionId || "", userId,
}; threadTitle: value,
organization,
threadSocket.emit("v1:thread:updateTitle", threadEdit); threadId: (val as CommentSchema).threadId || selectedComment.threadId,
} versionId: selectedVersion?.versionId || "",
} catch {} };
} else {
if (mode === "edit") { threadSocket.emit("v1:thread:updateTitle", threadEdit);
try { }
// const editComments = await addCommentsApi(projectId, value, selectedComment?.threadId, (val as Reply).replyId, selectedVersion?.versionId || "") } else if (mode === "edit") {
// if (!threadSocket?.active) {
// const commentData = { // API
// replyId: `${editComments.data?.replyId}`,
// creatorId: `${userId}`, editCommentsApi(projectId, value, selectedComment?.threadId, (val as Reply).replyId, selectedVersion?.versionId || "").then((editComments) => {
// createdAt: getRelativeTime(editComments.data?.createdAt), const commentData = {
// lastUpdatedAt: "2 hrs ago", replyId: `${editComments.data?.replyId}`,
// comment: value, creatorId: `${userId}`,
// } createdAt: getRelativeTime(editComments.data?.createdAt),
lastUpdatedAt: "2 hrs ago",
// updateReply((val as CommentSchema).threadId, (val as Reply)?.replyId, commentData); comment: value,
};
if (threadSocket) {
// projectId, userId, comment, organization, threadId updateReply((val as CommentSchema).threadId, (val as Reply)?.replyId, commentData);
const editComment = { setIsEditable(true);
projectId, setEditedThread(false);
userId, });
comment: value, } else {
organization, // SOCKET
threadId: selectedComment?.threadId,
commentId: (val as Reply).replyId ?? "", const editComment = {
versionId: selectedVersion?.versionId || "", projectId,
}; userId,
comment: value,
threadSocket.emit("v1-Comment:add", editComment); organization,
setIsEditable && setIsEditable(true); threadId: selectedComment?.threadId,
setEditedThread && setEditedThread(false); commentId: (val as Reply).replyId ?? "",
} versionId: selectedVersion?.versionId || "",
} catch {} };
}
} threadSocket.emit("v1-Comment:add", editComment);
// setValue(""); setIsEditable(true);
setIsEditComment(false); setEditedThread(false);
}; }
}
const handleDeleteAction = async (replyID: any) => { setIsEditComment(false);
if (!projectId || !selectedVersion) return; };
setOpenOptions(false);
try { const handleDeleteAction = async (replyID: any) => {
// const deletedComment = await deleteCommentApi(projectId, selectedComment?.threadId, (val as Reply).replyId , selectedVersion?.versionId || "") if (!projectId || !selectedVersion || !setMessages) return;
// setOpenOptions(false);
// if (deletedComment === "'Thread comment deleted Successfully'") {
// setMessages && setMessages(prev => prev.filter(message => message.replyId !== replyID)); if (!threadSocket?.active) {
// removeReply(val.creatorId, replyID) // API
// }
if (threadSocket && setMessages) { deleteCommentApi(projectId, selectedComment?.threadId, (val as Reply).replyId, selectedVersion?.versionId || "").then((deletedComment) => {
// projectId, userId, commentId, organization, threadId if (deletedComment === "'Thread comment deleted Successfully'") {
const deleteComment = { setMessages((prev) => prev.filter((message) => message.replyId !== replyID));
projectId, removeReply(val.creatorId, replyID);
userId, }
commentId: (val as Reply).replyId, });
organization, } else {
threadId: selectedComment?.threadId, // SOCKET
versionId: selectedVersion?.versionId || "",
}; const deleteComment = {
setMessages((prev) => { projectId,
// 👈 logs the current state userId,
return prev.filter((message) => message.replyId !== (val as Reply).replyId); commentId: (val as Reply).replyId,
}); organization,
removeReply(selectedComment?.threadId, (val as Reply).replyId); // Remove listener after response threadId: selectedComment?.threadId,
versionId: selectedVersion?.versionId || "",
threadSocket.emit("v1-Comment:delete", deleteComment); };
} setMessages((prev) => {
} catch {} return prev.filter((message) => message.replyId !== (val as Reply).replyId);
}; });
removeReply(selectedComment?.threadId, (val as Reply).replyId);
const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
requestAnimationFrame(() => { threadSocket.emit("v1-Comment:delete", deleteComment);
if (textareaRef.current) { }
const length = textareaRef.current.value.length; };
textareaRef.current.setSelectionRange(length, length);
} const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
}); requestAnimationFrame(() => {
}; if (textareaRef.current) {
const length = textareaRef.current.value.length;
return ( textareaRef.current.setSelectionRange(length, length);
<> }
{isEditComment ? ( });
<div className="edit-container"> };
<div className="input-container">
<textarea placeholder="type here" ref={textareaRef} autoFocus value={value} onChange={(e) => setValue(e.target.value)} style={{ resize: "none" }} onFocus={handleFocus} /> return (
</div> <>
<div className="actions-container"> {isEditComment ? (
<div className="options"></div> <div className="edit-container">
<div className="actions"> <div className="input-container">
<button <textarea placeholder="type here" ref={textareaRef} autoFocus value={value} onChange={(e) => setValue(e.target.value)} style={{ resize: "none" }} onFocus={handleFocus} />
className="cancel-button" </div>
onClick={() => { <div className="actions-container">
handleCancelAction(); <div className="options"></div>
}} <div className="actions">
> <button
Cancel className="cancel-button"
</button> onClick={() => {
<button handleCancelAction();
className="save-button" }}
onClick={() => { >
handleSaveAction(); Cancel
}} </button>
> <button
Save className="save-button"
</button> onClick={() => {
</div> handleSaveAction();
</div> }}
</div> >
) : ( Save
<div className="message-container"> </button>
<div className="profile" style={{ background: getAvatarColor(i, userName) }}> </div>
{userName?.charAt(0).toUpperCase() || "user"} </div>
</div> </div>
<div className="content"> ) : (
<div className="user-details"> <div className="message-container">
<div className="user-name">{userName}</div> <div className="profile" style={{ background: getAvatarColor(i, userName) }}>
<div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div> {userName?.charAt(0).toUpperCase() || "user"}
</div> </div>
{(val as Reply).creatorId === userId && ( <div className="content">
<div className="more-options" onMouseLeave={() => setOpenOptions(false)}> <div className="user-details">
<button <div className="user-name">{userName}</div>
className="more-options-button" <div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div>
onClick={() => { </div>
setOpenOptions(!openOptions); {(val as Reply).creatorId === userId && (
}} <div className="more-options" onMouseLeave={() => setOpenOptions(false)}>
> <button
<KebabIcon /> className="more-options-button"
</button> onClick={() => {
setOpenOptions(!openOptions);
{openOptions && ( }}
<div className="options-list"> >
<button <KebabIcon />
className="option" </button>
onClick={(e) => {
e.preventDefault(); {openOptions && (
setMode && setMode("edit"); <div className="options-list">
setOpenOptions(false); <button
setEditedThread && setEditedThread(true); className="option"
setIsEditComment(true); onClick={(e) => {
}} e.preventDefault();
> setMode && setMode("edit");
Edit setOpenOptions(false);
</button> setEditedThread && setEditedThread(true);
setIsEditComment(true);
{!isEditableThread && ( }}
<button >
className="option" Edit
onClick={() => { </button>
handleDeleteAction((val as Reply).replyId);
}} {!isEditableThread && (
> <button
Delete className="option"
</button> onClick={() => {
)} handleDeleteAction((val as Reply).replyId);
</div> }}
)} >
</div> Delete
)} </button>
)}
<div className="message">{"comment" in val ? val.comment : val.threadTitle}</div> </div>
</div> )}
</div> </div>
)} )}
</>
); <div className="message">{"comment" in val ? val.comment : val.threadTitle}</div>
}; </div>
</div>
export default Messages; )}
</>
);
};
export default Messages;

View File

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

View File

@@ -31,6 +31,7 @@ function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendere
useEffect(() => { useEffect(() => {
if (!fieldData && asset.eventData) { if (!fieldData && asset.eventData) {
getAssetFieldApi(asset.assetId).then((data) => { getAssetFieldApi(asset.assetId).then((data) => {
if (!data) return;
if (data.type === "ArmBot") { if (data.type === "ArmBot") {
if (data.data) { if (data.data) {
const fieldData: IK[] = data.data; const fieldData: IK[] = data.data;

View File

@@ -1,16 +1,17 @@
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/ui/usePlayButtonStore'; import { usePlayButtonStore } from "../../../../../store/ui/usePlayButtonStore";
import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads'; import CommentThreads from "../../../../../components/ui/collaboration/threads/CommentThreads";
import { useSelectedComment } from '../../../../../store/builder/store'; import { useSelectedComment } from "../../../../../store/builder/store";
import { Group, Object3D, Vector2, Vector3 } from 'three'; import { Group, Object3D, Vector3 } from "three";
import { useThree } from '@react-three/fiber'; import { useThree } from "@react-three/fiber";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
function CommentInstance({ comment }: { readonly comment: CommentSchema }) { function ThreadInstance({ comment }: { readonly comment: CommentSchema }) {
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const CommentRef = useRef(null); const CommentRef = useRef(null);
const [selectedObject, setSelectedObject] = useState<Object3D | null>(null); const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
const { selectedComment, setSelectedComment, setPosition2Dstate } = useSelectedComment() 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 groupRef = useRef<Group>(null);
const { size, camera } = useThree(); const { size, camera } = useThree();
@@ -33,28 +34,26 @@ function CommentInstance({ comment }: { readonly comment: CommentSchema }) {
const commentClicked = () => { const commentClicked = () => {
setSelectedComment(comment); setSelectedComment(comment);
const position = new Vector3(comment.position[0], comment.position[1], comment.position[2]) const position = new Vector3(comment.position[0], comment.position[1], comment.position[2]);
position.project(camera); position.project(camera);
const x = (position.x * 0.5 + 0.5) * size.width; const x = (position.x * 0.5 + 0.5) * size.width;
const y = (-(position.y * 0.5) + 0.2) * size.height; const y = (-(position.y * 0.5) + 0.2) * size.height;
setPosition2Dstate({ x, y }) setPosition2Dstate({ x, y });
if (groupRef.current) { if (groupRef.current) {
setSelectedObject(groupRef.current); setSelectedObject(groupRef.current);
} }
} };
useEffect(() => { useEffect(() => {
if (!selectedComment || selectedComment.threadId !== comment.threadId) { if (!selectedComment || selectedComment.threadId !== comment.threadId) {
setSelectedObject(null) setSelectedObject(null);
} }
}, [selectedComment]) }, [selectedComment]);
if (comment.state === "inactive" || isPlaying) return null;
if (comment.state === 'inactive' || isPlaying) return null;
return ( return (
<> <>
<group ref={groupRef} position={comment.position} rotation={comment.rotation}> <group ref={groupRef} position={comment.position} rotation={comment.rotation}>
<Html <Html
@@ -65,7 +64,7 @@ function CommentInstance({ comment }: { readonly comment: CommentSchema }) {
center center
// position={comment.position} // position={comment.position}
// rotation={comment.rotation} // rotation={comment.rotation}
className='comments-main-wrapper' className="comments-main-wrapper"
> >
<CommentThreads commentClicked={commentClicked} comment={comment} /> <CommentThreads commentClicked={commentClicked} comment={comment} />
</Html> </Html>
@@ -80,11 +79,7 @@ function CommentInstance({ comment }: { readonly comment: CommentSchema }) {
/> />
)} */} )} */}
</> </>
) );
} }
export default CommentInstance; export default ThreadInstance;

View File

@@ -1,6 +1,5 @@
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 { getAllThreads } from "../../../../services/factoryBuilder/comments/getAllThreads"; import { getAllThreads } from "../../../../services/factoryBuilder/comments/getAllThreads";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { getUserData } from "../../../../functions/getUserData"; import { getUserData } from "../../../../functions/getUserData";
@@ -8,10 +7,10 @@ import { getRelativeTime } from "../../../../components/ui/collaboration/functio
import { useSceneContext } from "../../../scene/sceneContext"; import { useSceneContext } from "../../../scene/sceneContext";
function CommentInstances() { function CommentInstances() {
const { comments, setComments } = useCommentStore();
const { projectId } = useParams(); const { projectId } = useParams();
const { userId } = getUserData(); const { userId } = getUserData();
const { versionStore } = useSceneContext(); const { versionStore, commentStore } = useSceneContext();
const { comments, setComments } = commentStore();
const { selectedVersion } = versionStore(); const { selectedVersion } = versionStore();
useEffect(() => { useEffect(() => {
@@ -42,7 +41,7 @@ function CommentInstances() {
.catch((err) => { .catch((err) => {
console.error("Failed to fetch threads:", err); console.error("Failed to fetch threads:", err);
}); });
}, []); }, [projectId, selectedVersion]);
return ( return (
<> <>

View File

@@ -1,30 +1,30 @@
import React, { useEffect } from 'react'; import React, { useEffect } from "react";
import { useSelectedComment, useSocketStore } from '../../../store/builder/store'; import { useSelectedComment, useSocketStore } from "../../../store/builder/store";
import { useCommentStore } from '../../../store/collaboration/useCommentStore'; import { getUserData } from "../../../functions/getUserData";
import { getUserData } from '../../../functions/getUserData'; import { getRelativeTime } from "../../../components/ui/collaboration/function/getRelativeTime";
import { getRelativeTime } from '../../../components/ui/collaboration/function/getRelativeTime'; import { useSceneContext } from "../../scene/sceneContext";
interface ThreadSocketProps { interface ThreadSocketProps {
setMessages: React.Dispatch<React.SetStateAction<Reply[]>>; setMessages: React.Dispatch<React.SetStateAction<Reply[]>>;
// mode: 'create' | 'edit' | null // mode: 'create' | 'edit' | null
modeRef: React.RefObject<'create' | 'edit' | null>; modeRef: React.RefObject<"create" | "edit" | null>;
messages: Reply[] messages: Reply[];
} }
const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSocketProps) => { const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSocketProps) => {
const { threadSocket } = useSocketStore(); const { threadSocket } = useSocketStore();
const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState } = useSelectedComment(); const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState } = useSelectedComment();
const { comments, removeReply, addComment, addReply, updateComment, updateReply } = useCommentStore(); const { commentStore } = useSceneContext();
const { comments, removeReply, addComment, addReply, updateComment, updateReply } = commentStore();
const { userId } = getUserData(); const { userId } = getUserData();
useEffect(() => { useEffect(() => {
if (!threadSocket) return; if (!threadSocket) return;
// --- Add Comment Handler --- // --- Add Comment Handler ---
// const handleAddComment = (data: any) => { // const handleAddComment = (data: any) => {
// //
// //
// const commentData = { // const commentData = {
// replyId: data.data?._id, // replyId: data.data?._id,
// creatorId: data.data?.userId, // creatorId: data.data?.userId,
@@ -32,11 +32,11 @@ const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSock
// lastUpdatedAt: '2 hrs ago', // lastUpdatedAt: '2 hrs ago',
// comment: data.data.comment, // comment: data.data.comment,
// }; // };
// // // //
// //
// if (mode == "create") { // if (mode == "create") {
// addReply(selectedComment?.threadId, commentData); // addReply(selectedComment?.threadId, commentData);
// //
// setMessages(prevMessages => [...prevMessages, commentData]); // setMessages(prevMessages => [...prevMessages, commentData]);
// } else if (mode == "edit") { // } else if (mode == "edit") {
// updateReply(selectedComment?.threadId, data.data?._id, commentData); // updateReply(selectedComment?.threadId, data.data?._id, commentData);
@@ -48,17 +48,14 @@ const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSock
// : message; // : message;
// }) // })
// ); // );
// //
// } else { // } else {
// //
// } // }
// threadSocket.off('v1-Comment:response:add', handleAddComment); // threadSocket.off('v1-Comment:response:add', handleAddComment);
// }; // };
// threadSocket.on('v1-Comment:response:add', handleAddComment); // threadSocket.on('v1-Comment:response:add', handleAddComment);
const handleAddComment = (data: any) => { const handleAddComment = (data: any) => {
const commentData = { const commentData = {
replyId: data.data?._id, replyId: data.data?._id,
creatorId: data.data?.userId, creatorId: data.data?.userId,
@@ -75,27 +72,21 @@ const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSock
setMessages((prev) => setMessages((prev) =>
prev.map((message) => { prev.map((message) => {
// 👈 log each message // 👈 log each message
return message.replyId === data.data?._id return message.replyId === data.data?._id ? { ...message, comment: data.data?.comment } : message;
? { ...message, comment: data.data?.comment }
: message;
}) })
); );
} }
}; };
threadSocket.on('v1-Comment:response:add', handleAddComment); threadSocket.on("v1-Comment:response:add", handleAddComment);
// --- Delete Comment Handler --- // --- Delete Comment Handler ---
const handleDeleteComment = (data: any) => { const handleDeleteComment = (data: any) => {};
}; threadSocket.on("v1-Comment:response:delete", handleDeleteComment);
threadSocket.on('v1-Comment:response:delete', handleDeleteComment);
// --- Create Thread Handler --- // --- Create Thread Handler ---
const handleCreateThread = (data: any) => { const handleCreateThread = (data: any) => {
const comment: CommentSchema = { const comment: CommentSchema = {
state: 'active', state: "active",
threadId: data.data?._id, threadId: data.data?._id,
creatorId: userId || data.data?.userId, creatorId: userId || data.data?.userId,
createdAt: data.data?.createdAt, createdAt: data.data?.createdAt,
@@ -105,32 +96,26 @@ const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSock
rotation: [0, 0, 0], rotation: [0, 0, 0],
comments: [], comments: [],
}; };
setSelectedComment(comment) setSelectedComment(comment);
addComment(comment); addComment(comment);
setCommentPositionState(null); setCommentPositionState(null);
// setSelectedComment(null); // setSelectedComment(null);
}; };
threadSocket.on('v1-thread:response:create', handleCreateThread); threadSocket.on("v1-thread:response:create", handleCreateThread);
// --- Delete Thread Handler --- // --- Delete Thread Handler ---
// const handleDeleteThread = (data: any) => { // const handleDeleteThread = (data: any) => {
// //
// }; // };
// threadSocket.on("v1-thread:response:delete", handleDeleteThread); // threadSocket.on("v1-thread:response:delete", handleDeleteThread);
const handleDeleteThread = (data: any) => {
}; const handleDeleteThread = (data: any) => {};
threadSocket.on("v1-thread:response:delete", handleDeleteThread); threadSocket.on("v1-thread:response:delete", handleDeleteThread);
const handleEditThread = (data: any) => { const handleEditThread = (data: any) => {
const editedThread: CommentSchema = { const editedThread: CommentSchema = {
state: 'active', state: "active",
threadId: data.data?._id, threadId: data.data?._id,
creatorId: userId, creatorId: userId,
createdAt: data.data?.createdAt, createdAt: data.data?.createdAt,
@@ -141,23 +126,37 @@ const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSock
comments: data.data?.comments, comments: data.data?.comments,
}; };
// //
updateComment(data.data?._id, editedThread); updateComment(data.data?._id, editedThread);
setSelectedComment(editedThread) setSelectedComment(editedThread);
// setSelectedComment(null); // setSelectedComment(null);
}; };
threadSocket.on('v1-thread:response:updateTitle', handleEditThread); threadSocket.on("v1-thread:response:updateTitle", handleEditThread);
// Cleanup on unmount // Cleanup on unmount
return () => { return () => {
threadSocket.off('v1-Comment:response:add', handleAddComment); threadSocket.off("v1-Comment:response:add", handleAddComment);
threadSocket.off('v1-Comment:response:delete', handleDeleteComment); threadSocket.off("v1-Comment:response:delete", handleDeleteComment);
threadSocket.off('v1-thread:response:create', handleCreateThread); threadSocket.off("v1-thread:response:create", handleCreateThread);
threadSocket.off('v1-thread:response:delete', handleDeleteThread); threadSocket.off("v1-thread:response:delete", handleDeleteThread);
threadSocket.off('v1-thread:response:updateTitle', handleEditThread); threadSocket.off("v1-thread:response:updateTitle", handleEditThread);
}; };
}, [threadSocket, selectedComment, commentPositionState, userId, setMessages, addReply, addComment, setSelectedComment, setCommentPositionState, updateComment, comments, modeRef.current, messages]); }, [
threadSocket,
selectedComment,
commentPositionState,
userId,
setMessages,
addReply,
addComment,
setSelectedComment,
setCommentPositionState,
updateComment,
comments,
modeRef.current,
messages,
]);
return null; return null;
}; };

View File

@@ -1,45 +1,46 @@
import { createContext, useContext, useMemo, useRef } from 'react'; import { createContext, useContext, useMemo, useRef } from "react";
import { createVersionStore, VersionStoreType } from '../../store/builder/useVersionStore'; import { createVersionStore, VersionStoreType } from "../../store/builder/useVersionStore";
import { createAssetStore, AssetStoreType } from '../../store/builder/useAssetStore'; import { createAssetStore, AssetStoreType } from "../../store/builder/useAssetStore";
import { createWallAssetStore, WallAssetStoreType } from '../../store/builder/useWallAssetStore'; import { createWallAssetStore, WallAssetStoreType } from "../../store/builder/useWallAssetStore";
import { createWallStore, WallStoreType } from '../../store/builder/useWallStore'; import { createWallStore, WallStoreType } from "../../store/builder/useWallStore";
import { createAisleStore, AisleStoreType } from '../../store/builder/useAisleStore'; import { createAisleStore, AisleStoreType } from "../../store/builder/useAisleStore";
import { createZoneStore, ZoneStoreType } from '../../store/builder/useZoneStore'; import { createZoneStore, ZoneStoreType } from "../../store/builder/useZoneStore";
import { createFloorStore, FloorStoreType } from '../../store/builder/useFloorStore'; import { createFloorStore, FloorStoreType } from "../../store/builder/useFloorStore";
import { createUndoRedo2DStore, UndoRedo2DStoreType } from '../../store/builder/useUndoRedo2DStore'; import { createUndoRedo2DStore, UndoRedo2DStoreType } from "../../store/builder/useUndoRedo2DStore";
import { createUndoRedo3DStore, UndoRedo3DStoreType } from '../../store/builder/useUndoRedo3DStore'; import { createUndoRedo3DStore, UndoRedo3DStoreType } from "../../store/builder/useUndoRedo3DStore";
import { createEventStore, EventStoreType } from '../../store/simulation/useEventsStore'; import { createEventStore, EventStoreType } from "../../store/simulation/useEventsStore";
import { createProductStore, ProductStoreType } from '../../store/simulation/useProductStore'; import { createProductStore, ProductStoreType } from "../../store/simulation/useProductStore";
import { createMaterialStore, MaterialStoreType } from '../../store/simulation/useMaterialStore'; import { createMaterialStore, MaterialStoreType } from "../../store/simulation/useMaterialStore";
import { createArmBotStore, ArmBotStoreType } from '../../store/simulation/useArmBotStore'; import { createArmBotStore, ArmBotStoreType } from "../../store/simulation/useArmBotStore";
import { createMachineStore, MachineStoreType } from '../../store/simulation/useMachineStore'; import { createMachineStore, MachineStoreType } from "../../store/simulation/useMachineStore";
import { createConveyorStore, ConveyorStoreType } from '../../store/simulation/useConveyorStore'; import { createConveyorStore, ConveyorStoreType } from "../../store/simulation/useConveyorStore";
import { createVehicleStore, VehicleStoreType } from '../../store/simulation/useVehicleStore'; import { createVehicleStore, VehicleStoreType } from "../../store/simulation/useVehicleStore";
import { createStorageUnitStore, StorageUnitStoreType } from '../../store/simulation/useStorageUnitStore'; import { createStorageUnitStore, StorageUnitStoreType } from "../../store/simulation/useStorageUnitStore";
import { createHumanStore, HumanStoreType } from '../../store/simulation/useHumanStore'; import { createHumanStore, HumanStoreType } from "../../store/simulation/useHumanStore";
import { createCraneStore, CraneStoreType } from '../../store/simulation/useCraneStore'; import { createCraneStore, CraneStoreType } from "../../store/simulation/useCraneStore";
import { createCommentsStore, CommentStoreType } from "../../store/collaboration/useCommentStore";
type SceneContextValue = { type SceneContextValue = {
versionStore: VersionStoreType;
versionStore: VersionStoreType assetStore: AssetStoreType;
wallAssetStore: WallAssetStoreType;
wallStore: WallStoreType;
aisleStore: AisleStoreType;
zoneStore: ZoneStoreType;
floorStore: FloorStoreType;
assetStore: AssetStoreType, undoRedo2DStore: UndoRedo2DStoreType;
wallAssetStore: WallAssetStoreType, undoRedo3DStore: UndoRedo3DStoreType;
wallStore: WallStoreType,
aisleStore: AisleStoreType,
zoneStore: ZoneStoreType,
floorStore: FloorStoreType,
undoRedo2DStore: UndoRedo2DStoreType, eventStore: EventStoreType;
undoRedo3DStore: UndoRedo3DStoreType, productStore: ProductStoreType;
eventStore: EventStoreType,
productStore: ProductStoreType,
materialStore: MaterialStoreType; materialStore: MaterialStoreType;
armBotStore: ArmBotStoreType; armBotStore: ArmBotStoreType;
@@ -50,24 +51,19 @@ type SceneContextValue = {
humanStore: HumanStoreType; humanStore: HumanStoreType;
craneStore: CraneStoreType; craneStore: CraneStoreType;
commentStore: CommentStoreType;
humanEventManagerRef: React.RefObject<HumanEventManagerState>; humanEventManagerRef: React.RefObject<HumanEventManagerState>;
craneEventManagerRef: React.RefObject<CraneEventManagerState>; craneEventManagerRef: React.RefObject<CraneEventManagerState>;
clearStores: () => void; clearStores: () => void;
layout: 'Main Layout' | 'Comparison Layout'; layout: "Main Layout" | "Comparison Layout";
}; };
const SceneContext = createContext<SceneContextValue | null>(null); const SceneContext = createContext<SceneContextValue | null>(null);
export function SceneProvider({ export function SceneProvider({ children, layout }: { readonly children: React.ReactNode; readonly layout: "Main Layout" | "Comparison Layout" }) {
children,
layout
}: {
readonly children: React.ReactNode;
readonly layout: 'Main Layout' | 'Comparison Layout';
}) {
const versionStore = useMemo(() => createVersionStore(), []); const versionStore = useMemo(() => createVersionStore(), []);
const assetStore = useMemo(() => createAssetStore(), []); const assetStore = useMemo(() => createAssetStore(), []);
@@ -92,35 +88,62 @@ export function SceneProvider({
const humanStore = useMemo(() => createHumanStore(), []); const humanStore = useMemo(() => createHumanStore(), []);
const craneStore = useMemo(() => createCraneStore(), []); const craneStore = useMemo(() => createCraneStore(), []);
const commentStore = useMemo(() => createCommentsStore(), []);
const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] }); const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] });
const craneEventManagerRef = useRef<CraneEventManagerState>({ craneStates: [] }); const craneEventManagerRef = useRef<CraneEventManagerState>({ craneStates: [] });
const clearStores = useMemo(() => () => { const clearStores = useMemo(
versionStore.getState().clearVersions(); () => () => {
assetStore.getState().clearAssets(); versionStore.getState().clearVersions();
wallAssetStore.getState().clearWallAssets(); assetStore.getState().clearAssets();
wallStore.getState().clearWalls(); wallAssetStore.getState().clearWallAssets();
aisleStore.getState().clearAisles(); wallStore.getState().clearWalls();
zoneStore.getState().clearZones(); aisleStore.getState().clearAisles();
floorStore.getState().clearFloors(); zoneStore.getState().clearZones();
undoRedo2DStore.getState().clearUndoRedo2D(); floorStore.getState().clearFloors();
undoRedo3DStore.getState().clearUndoRedo3D(); undoRedo2DStore.getState().clearUndoRedo2D();
eventStore.getState().clearEvents(); undoRedo3DStore.getState().clearUndoRedo3D();
productStore.getState().clearProducts(); eventStore.getState().clearEvents();
materialStore.getState().clearMaterials(); productStore.getState().clearProducts();
armBotStore.getState().clearArmBots(); materialStore.getState().clearMaterials();
machineStore.getState().clearMachines(); armBotStore.getState().clearArmBots();
conveyorStore.getState().clearConveyors(); machineStore.getState().clearMachines();
vehicleStore.getState().clearVehicles(); conveyorStore.getState().clearConveyors();
storageUnitStore.getState().clearStorageUnits(); vehicleStore.getState().clearVehicles();
humanStore.getState().clearHumans(); storageUnitStore.getState().clearStorageUnits();
craneStore.getState().clearCranes(); humanStore.getState().clearHumans();
humanEventManagerRef.current.humanStates = []; craneStore.getState().clearCranes();
craneEventManagerRef.current.craneStates = []; commentStore.getState().clearComments();
}, [versionStore, assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, undoRedo3DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore]); humanEventManagerRef.current.humanStates = [];
craneEventManagerRef.current.craneStates = [];
},
[
versionStore,
assetStore,
wallAssetStore,
wallStore,
aisleStore,
zoneStore,
undoRedo2DStore,
undoRedo3DStore,
floorStore,
eventStore,
productStore,
materialStore,
armBotStore,
machineStore,
conveyorStore,
vehicleStore,
storageUnitStore,
humanStore,
craneStore,
commentStore,
]
);
const contextValue = useMemo(() => ( const contextValue = useMemo(
{ () => ({
versionStore, versionStore,
assetStore, assetStore,
wallAssetStore, wallAssetStore,
@@ -140,25 +163,46 @@ export function SceneProvider({
storageUnitStore, storageUnitStore,
humanStore, humanStore,
craneStore, craneStore,
commentStore,
humanEventManagerRef, humanEventManagerRef,
craneEventManagerRef, craneEventManagerRef,
clearStores, clearStores,
layout layout,
} }),
), [versionStore, assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, undoRedo3DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, clearStores, layout]); [
versionStore,
return ( assetStore,
<SceneContext.Provider value={contextValue}> wallAssetStore,
{children} wallStore,
</SceneContext.Provider> aisleStore,
zoneStore,
floorStore,
undoRedo2DStore,
undoRedo3DStore,
eventStore,
productStore,
materialStore,
armBotStore,
machineStore,
conveyorStore,
vehicleStore,
storageUnitStore,
humanStore,
craneStore,
commentStore,
clearStores,
layout,
]
); );
return <SceneContext.Provider value={contextValue}>{children}</SceneContext.Provider>;
} }
// Base hook to get the context // Base hook to get the context
export function useSceneContext() { export function useSceneContext() {
const context = useContext(SceneContext); const context = useContext(SceneContext);
if (!context) { if (!context) {
throw new Error('useSceneContext must be used within a SceneProvider'); throw new Error("useSceneContext must be used within a SceneProvider");
} }
return context; return context;
} }

View File

@@ -0,0 +1,31 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const addCommentsApi = async (projectId: any, comment: string, threadId: string, versionId: string) => {
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, versionId }),
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
echo.error("Failed to add comment");
}
const result = await response.json();
return result;
} catch {
echo.error("Failed to add comment");
}
};

View File

@@ -1,41 +0,0 @@
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,
versionId: string
) => {
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, versionId }),
}
);
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
echo.error("Failed to add comment");
}
const result = await response.json();
return result;
} catch {
echo.error("Failed to add comment");
}
};

View File

@@ -0,0 +1,31 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const editCommentsApi = async (projectId: any, comment: string, commentId: string, threadId: string, versionId: string) => {
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, commentId, threadId, versionId }),
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
echo.error("Failed to edit comment");
}
const result = await response.json();
return result;
} catch {
echo.error("Failed to edit comment");
}
};

View File

@@ -24,10 +24,12 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
reconnection: true, reconnection: true,
auth: { token, refreshToken }, auth: { token, refreshToken },
}); });
const projectSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/project`, { const projectSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/project`, {
reconnection: true, reconnection: true,
auth: { token, refreshToken }, auth: { token, refreshToken },
}); });
const threadSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`, { const threadSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`, {
reconnection: true, reconnection: true,
auth: { token, refreshToken }, auth: { token, refreshToken },

View File

@@ -1,6 +1,6 @@
import { Object3D } from 'three'; import { Object3D } from "three";
import { create } from 'zustand'; import { create } from "zustand";
import { immer } from 'zustand/middleware/immer'; import { immer } from "zustand/middleware/immer";
interface AssetsStore { interface AssetsStore {
assets: Assets; assets: Assets;
@@ -44,8 +44,8 @@ interface AssetsStore {
removeAnimation: (modelUuid: string, animation: string) => void; removeAnimation: (modelUuid: string, animation: string) => void;
// Event data operations // Event data operations
addEventData: (modelUuid: string, eventData: Asset['eventData']) => void; addEventData: (modelUuid: string, eventData: Asset["eventData"]) => void;
updateEventData: (modelUuid: string, updates: Partial<Asset['eventData']>) => void; updateEventData: (modelUuid: string, updates: Partial<Asset["eventData"]>) => void;
removeEventData: (modelUuid: string) => void; removeEventData: (modelUuid: string) => void;
// Helper functions // Helper functions
@@ -68,7 +68,7 @@ export const createAssetStore = () => {
// Asset CRUD operations // Asset CRUD operations
addAsset: (asset) => { addAsset: (asset) => {
set((state) => { set((state) => {
if (!state.assets.some(a => a.modelUuid === asset.modelUuid)) { if (!state.assets.some((a) => a.modelUuid === asset.modelUuid)) {
state.assets.push(asset); state.assets.push(asset);
} }
}); });
@@ -76,13 +76,13 @@ export const createAssetStore = () => {
removeAsset: (modelUuid) => { removeAsset: (modelUuid) => {
set((state) => { set((state) => {
state.assets = state.assets.filter(a => a.modelUuid !== modelUuid); state.assets = state.assets.filter((a) => a.modelUuid !== modelUuid);
}); });
}, },
updateAsset: (modelUuid, updates) => { updateAsset: (modelUuid, updates) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
Object.assign(asset, updates); Object.assign(asset, updates);
} }
@@ -96,7 +96,7 @@ export const createAssetStore = () => {
}, },
resetAsset: (modelUuid) => { resetAsset: (modelUuid) => {
const asset = get().assets.find(a => a.modelUuid === modelUuid); const asset = get().assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
const clonedAsset = JSON.parse(JSON.stringify(asset)); const clonedAsset = JSON.parse(JSON.stringify(asset));
setTimeout(() => { setTimeout(() => {
@@ -158,7 +158,7 @@ export const createAssetStore = () => {
// Asset properties // Asset properties
setName: (modelUuid, newName) => { setName: (modelUuid, newName) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.modelName = newName; asset.modelName = newName;
} }
@@ -167,7 +167,7 @@ export const createAssetStore = () => {
setPosition: (modelUuid, position) => { setPosition: (modelUuid, position) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.position = position; asset.position = position;
} }
@@ -176,7 +176,7 @@ export const createAssetStore = () => {
setRotation: (modelUuid, rotation) => { setRotation: (modelUuid, rotation) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.rotation = rotation; asset.rotation = rotation;
} }
@@ -185,7 +185,7 @@ export const createAssetStore = () => {
setLock: (modelUuid, isLocked) => { setLock: (modelUuid, isLocked) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.isLocked = isLocked; asset.isLocked = isLocked;
} }
@@ -194,7 +194,7 @@ export const createAssetStore = () => {
setCollision: (modelUuid, isCollidable) => { setCollision: (modelUuid, isCollidable) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.isCollidable = isCollidable; asset.isCollidable = isCollidable;
} }
@@ -203,7 +203,7 @@ export const createAssetStore = () => {
setVisibility: (modelUuid, isVisible) => { setVisibility: (modelUuid, isVisible) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.isVisible = isVisible; asset.isVisible = isVisible;
} }
@@ -212,7 +212,7 @@ export const createAssetStore = () => {
setOpacity: (modelUuid, opacity) => { setOpacity: (modelUuid, opacity) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.opacity = opacity; asset.opacity = opacity;
} }
@@ -222,11 +222,11 @@ export const createAssetStore = () => {
// Animation controls // Animation controls
setAnimations: (modelUuid, animations) => { setAnimations: (modelUuid, animations) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.animations = animations; asset.animations = animations;
if (!asset.animationState) { if (!asset.animationState) {
asset.animationState = { current: '', isPlaying: false, loopAnimation: true, isCompleted: true }; asset.animationState = { current: "", isPlaying: false, loopAnimation: true, isCompleted: true };
} }
} }
}); });
@@ -234,7 +234,7 @@ export const createAssetStore = () => {
setCurrentAnimation: (modelUuid, current, isPlaying, loopAnimation, isCompleted) => { setCurrentAnimation: (modelUuid, current, isPlaying, loopAnimation, isCompleted) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset?.animationState) { if (asset?.animationState) {
asset.animationState.current = current; asset.animationState.current = current;
asset.animationState.isPlaying = isPlaying; asset.animationState.isPlaying = isPlaying;
@@ -246,7 +246,7 @@ export const createAssetStore = () => {
setAnimationComplete: (modelUuid, isCompleted) => { setAnimationComplete: (modelUuid, isCompleted) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset?.animationState) { if (asset?.animationState) {
asset.animationState.isCompleted = isCompleted; asset.animationState.isCompleted = isCompleted;
} }
@@ -255,9 +255,9 @@ export const createAssetStore = () => {
resetAnimation: (modelUuid) => { resetAnimation: (modelUuid) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset?.animationState) { if (asset?.animationState) {
asset.animationState.current = ''; asset.animationState.current = "";
asset.animationState.isPlaying = true; asset.animationState.isPlaying = true;
asset.animationState.loopAnimation = true; asset.animationState.loopAnimation = true;
asset.animationState.isCompleted = true; asset.animationState.isCompleted = true;
@@ -267,7 +267,7 @@ export const createAssetStore = () => {
addAnimation: (modelUuid, animation) => { addAnimation: (modelUuid, animation) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
if (!asset.animations) { if (!asset.animations) {
asset.animations = [animation]; asset.animations = [animation];
@@ -280,12 +280,12 @@ export const createAssetStore = () => {
removeAnimation: (modelUuid, animation) => { removeAnimation: (modelUuid, animation) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset?.animations) { if (asset?.animations) {
asset.animations = asset.animations.filter(a => a !== animation); asset.animations = asset.animations.filter((a) => a !== animation);
if (asset.animationState?.current === animation) { if (asset.animationState?.current === animation) {
asset.animationState.isPlaying = false; asset.animationState.isPlaying = false;
asset.animationState.current = ''; asset.animationState.current = "";
} }
} }
}); });
@@ -294,7 +294,7 @@ export const createAssetStore = () => {
// Event data operations // Event data operations
addEventData: (modelUuid, eventData) => { addEventData: (modelUuid, eventData) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
asset.eventData = eventData; asset.eventData = eventData;
} }
@@ -303,7 +303,7 @@ export const createAssetStore = () => {
updateEventData: (modelUuid, updates) => { updateEventData: (modelUuid, updates) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset?.eventData) { if (asset?.eventData) {
asset.eventData = { ...asset.eventData, ...updates }; asset.eventData = { ...asset.eventData, ...updates };
} }
@@ -312,7 +312,7 @@ export const createAssetStore = () => {
removeEventData: (modelUuid) => { removeEventData: (modelUuid) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find((a) => a.modelUuid === modelUuid);
if (asset) { if (asset) {
delete asset.eventData; delete asset.eventData;
} }
@@ -321,21 +321,18 @@ export const createAssetStore = () => {
// Helper functions // Helper functions
getAssetById: (modelUuid) => { getAssetById: (modelUuid) => {
return get().assets.find(a => a.modelUuid === modelUuid); return get().assets.find((a) => a.modelUuid === modelUuid);
}, },
getAssetByPointUuid: (pointUuid) => { getAssetByPointUuid: (pointUuid) => {
return get().assets.find(asset => return get().assets.find((asset) => asset.eventData?.point?.uuid === pointUuid || asset.eventData?.points?.some((p) => p.uuid === pointUuid));
asset.eventData?.point?.uuid === pointUuid ||
asset.eventData?.points?.some(p => p.uuid === pointUuid)
);
}, },
hasAsset: (modelUuid) => { hasAsset: (modelUuid) => {
return get().assets.some(a => a.modelUuid === modelUuid); return get().assets.some((a) => a.modelUuid === modelUuid);
} },
})) }))
) );
} };
export type AssetStoreType = ReturnType<typeof createAssetStore>; export type AssetStoreType = ReturnType<typeof createAssetStore>;

View File

@@ -4,90 +4,94 @@ import { immer } from "zustand/middleware/immer";
interface CommentStore { interface CommentStore {
comments: CommentsSchema; comments: CommentsSchema;
// Comment operations
addComment: (comment: CommentSchema) => void; addComment: (comment: CommentSchema) => void;
setComments: (comments: CommentsSchema) => void; setComments: (comments: CommentsSchema) => void;
updateComment: (threadId: string, updates: Partial<CommentSchema>) => void; updateComment: (threadId: string, updates: Partial<CommentSchema>) => void;
removeComment: (threadId: string) => void; removeComment: (threadId: string) => void;
clearComments: () => void;
// Reply operations
addReply: (threadId: string, reply: Reply) => void; addReply: (threadId: string, reply: Reply) => void;
updateReply: (threadId: string, replyId: string, updates: Partial<Reply>) => void; updateReply: (threadId: string, replyId: string, updates: Partial<Reply>) => void;
removeReply: (threadId: string, _id: string) => void; removeReply: (threadId: string, _id: string) => void;
// Getters
getCommentById: (threadId: string) => CommentSchema | undefined; getCommentById: (threadId: string) => CommentSchema | undefined;
} }
export const useCommentStore = create<CommentStore>()( export const createCommentsStore = () => {
immer((set, get) => ({ return create<CommentStore>()(
comments: [], immer((set, get) => ({
comments: [],
// Comment operations addComment: (comment) => {
addComment: (comment) => { set((state) => {
set((state) => { if (!state.comments.find((c) => c.threadId === comment.threadId)) {
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: (threadId, updates) => { updateComment: (threadId, updates) => {
console.log("threadId:updater ", threadId); set((state) => {
set((state) => { const comment = state.comments.find((c) => c.threadId === threadId);
const comment = state.comments.find((c) => c.threadId === threadId);
if (comment) {
Object.assign(comment, updates);
}
});
},
removeComment: (threadId) => {
set((state) => {
state.comments = state.comments.filter((c) => c.threadId !== threadId);
});
},
// Reply operations
addReply: (threadId, comment) => {
set((state) => {
const reply = state.comments.find((c) => c.threadId === threadId);
if (reply) {
reply.comments.push(comment);
}
});
},
updateReply: (threadId, replyId, updates) => {
set((state) => {
const reply = state.comments.find((c) => c.threadId === threadId);
if (reply) {
const comment = reply.comments.find((r) => r.replyId === replyId);
if (comment) { if (comment) {
Object.assign(comment, updates); Object.assign(comment, updates);
} }
} });
}); },
},
removeReply: (threadId, _id) => { removeComment: (threadId) => {
set((state) => { set((state) => {
const comment = state.comments.find((c) => c.threadId === threadId); state.comments = state.comments.filter((c) => c.threadId !== threadId);
if (comment) { });
comment.comments = comment.comments.filter((r) => r.replyId !== _id); },
}
});
},
// Getter clearComments: () => {
getCommentById: (threadId) => { set((state) => {
return get().comments.find((c) => c.threadId === threadId); state.comments = [];
}, });
})) },
);
addReply: (threadId, comment) => {
set((state) => {
const reply = state.comments.find((c) => c.threadId === threadId);
if (reply) {
reply.comments.push(comment);
}
});
},
updateReply: (threadId, replyId, updates) => {
set((state) => {
const reply = state.comments.find((c) => c.threadId === threadId);
if (reply) {
const comment = reply.comments.find((r) => r.replyId === replyId);
if (comment) {
Object.assign(comment, updates);
}
}
});
},
removeReply: (threadId, _id) => {
set((state) => {
const comment = state.comments.find((c) => c.threadId === threadId);
if (comment) {
comment.comments = comment.comments.filter((r) => r.replyId !== _id);
}
});
},
getCommentById: (threadId) => {
return get().comments.find((c) => c.threadId === threadId);
},
}))
);
};
export type CommentStoreType = ReturnType<typeof createCommentsStore>;