refactoring thread functionalities including socketresponses

This commit is contained in:
2025-10-22 12:13:00 +05:30
parent cbec3f5c97
commit 6ce16483c6
25 changed files with 909 additions and 421 deletions

View File

@@ -1,6 +1,13 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useLoadingProgress, useRenameModeStore, useIsComparing, useSelectedComment, useWidgetSubOption, useToggleView, useCreateNewWindow } from "../../../store/builder/store";
import {
useLoadingProgress,
useRenameModeStore,
useIsComparing,
useWidgetSubOption,
useToggleView,
useCreateNewWindow,
} from "../../../store/builder/store";
import useModuleStore from "../../../store/ui/useModuleStore";
import { useSocketStore } from "../../../store/socket/useSocketStore";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
@@ -44,16 +51,17 @@ function MainScene() {
const { toggleView } = useToggleView();
const { isPlaying } = usePlayButtonStore();
const { widgetSubOption } = useWidgetSubOption();
const { builderSocket, visualizationSocket } = useSocketStore();
const { builderSocket, visualizationSocket, threadSocket } = useSocketStore();
const { selectedZone } = useSelectedZoneStore();
const { setFloatingWidget } = useFloatingWidget();
const { assetStore, productStore, versionStore } = useSceneContext();
const { assetStore, productStore, versionStore, threadStore } = useSceneContext();
const { products, selectedProduct } = productStore();
const { versionHistory, setVersions, selectedVersion, setSelectedVersion } = versionStore();
const { setName, selectedAssets, setSelectedAssets } = assetStore();
const { projectId } = useParams();
const { isRenameMode, setIsRenameMode } = useRenameModeStore();
const { selectedComment, commentPositionState } = useSelectedComment();
const { commentPositionState, selectedThread } = threadStore();
const { resetStates } = useRestStates();
const { organization, userId } = getUserData();
const { createNewWindow } = useCreateNewWindow();
@@ -71,7 +79,12 @@ function MainScene() {
builderSocket.emit("joinRoom", { projectId: projectId });
}, 1000);
}
}, [builderSocket, projectId]);
if (threadSocket && projectId) {
setTimeout(() => {
threadSocket.emit("joinRoom", { projectId: projectId });
}, 1000);
}
}, [builderSocket, threadSocket, projectId]);
useEffect(() => {
if (activeModule !== "simulation") {
@@ -108,9 +121,13 @@ function MainScene() {
useEffect(() => {
if (versionHistory.length > 0) {
recentlyViewedApi().then((projects) => {
const recent_opened_verisionID = (Object.values(projects?.RecentlyViewed || {})[0] as any)?.Present_version._id;
const recent_opened_verisionID = (
Object.values(projects?.RecentlyViewed || {})[0] as any
)?.Present_version._id;
if (recent_opened_verisionID && projects.RecentlyViewed[0]._id === projectId) {
const version = versionHistory.find((ver) => ver.versionId === recent_opened_verisionID);
const version = versionHistory.find(
(ver) => ver.versionId === recent_opened_verisionID
);
if (version) {
setSelectedVersion(version);
}
@@ -188,7 +205,9 @@ function MainScene() {
{!selectedUser && (
<>
<KeyPressListener />
{!createNewWindow && loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
{!createNewWindow && loadingProgress > 0 && (
<LoadingPage progress={loadingProgress} />
)}
{!isPlaying && (
<>
{!toggleView && !isComparing && <ModuleToggle />}
@@ -203,27 +222,48 @@ function MainScene() {
{/* <RealTimeVisulization /> */}
{activeModule === "market" && <MarketPlace />}
{activeModule !== "market" && !isPlaying && !isComparing && <Tools />}
{isPlaying && activeModule === "simulation" && loadingProgress === 0 && <SimulationPlayer />}
{isPlaying && activeModule === "simulation" && loadingProgress === 0 && (
<SimulationPlayer />
)}
{isPlaying && activeModule !== "simulation" && <ControlsPlayer />}
{activeModule === "visualization" && <DashboardEditor />}
{isRenameMode && selectedAssets.length === 1 && <RenameTooltip name={selectedAssets[0].userData.modelName} onSubmit={handleObjectRename} />}
{isRenameMode && selectedAssets.length === 1 && (
<RenameTooltip
name={selectedAssets[0].userData.modelName}
onSubmit={handleObjectRename}
/>
)}
{activeModule === "builder" && toggleView && <SelectFloorPlan />}
{selectedProduct && selectedVersion && isComparing && !isPlaying && activeModule === "simulation" && (
<div className="selectLayout-wrapper">
<RegularDropDown header={selectedVersion.versionName} options={versionHistory.map((v) => v.versionName)} onSelect={handleSelectVersion} search={false} />
<br />
<RegularDropDown header={selectedProduct.productName} options={products.map((l) => l.productName)} onSelect={handleSelectProduct} search={false} />
</div>
)}
{selectedProduct &&
selectedVersion &&
isComparing &&
!isPlaying &&
activeModule === "simulation" && (
<div className="selectLayout-wrapper">
<RegularDropDown
header={selectedVersion.versionName}
options={versionHistory.map((v) => v.versionName)}
onSelect={handleSelectVersion}
search={false}
/>
<br />
<RegularDropDown
header={selectedProduct.productName}
options={products.map((l) => l.productName)}
onSelect={handleSelectProduct}
search={false}
/>
</div>
)}
<VersionSaved />
</>
)}
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat />}
{(commentPositionState !== null || selectedThread !== null) && <ThreadChat />}
{activeModule !== "market" && !selectedUser && <Footer />}

View File

@@ -31,10 +31,10 @@ import SelectedFloorProperties from "./properties/SelectedFloorProperties";
import SelectedDecalProperties from "./properties/SelectedDecalProperties";
import SelectedAisleProperties from "./properties/SelectedAisleProperties";
import SelectedZoneProperties from "./properties/SelectedZoneProperties";
import ThreadProperties from "./properties/ThreadProperties";
import ResourceManagement from "./resourceManagement/ResourceManagement";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import ThreadDetails from "./properties/eventProperties/components/ThreadDetails";
type DisplayComponent =
| "versionHistory"
@@ -54,13 +54,12 @@ type DisplayComponent =
| "analysis"
| "visualization"
| "resourceManagement"
| "threadDetails"
| "threadProperties"
| "none";
const SideBarRight: React.FC = () => {
const { activeModule } = useModuleStore();
const { activeTool } = useActiveTool();
console.log("activeTool: ", activeTool);
const { toggleUIRight } = useToggleStore();
const { toolMode } = useToolMode();
const { subModule, setSubModule } = useSubModuleStore();
@@ -68,12 +67,10 @@ const SideBarRight: React.FC = () => {
useBuilderStore();
const { selectedEventData } = useSelectedEventData();
const { selectedEventSphere } = useSelectedEventSphere();
const { versionStore, assetStore, threadStore } = useSceneContext();
const { versionStore, assetStore } = useSceneContext();
const { selectedAssets } = assetStore();
const { viewVersionHistory, setVersionHistoryVisible } = versionStore();
const { isComparing } = useIsComparing();
const { threads } = threadStore();
console.log("threads: ", threads);
const [displayComponent, setDisplayComponent] = useState<DisplayComponent>("none");
@@ -119,7 +116,12 @@ const SideBarRight: React.FC = () => {
return;
}
}
if (
(activeModule === "builder" || activeModule === "simulation") &&
activeTool === "comment"
) {
setDisplayComponent("threadProperties");
}
if (
activeModule === "simulation" ||
(activeModule === "builder" && activeTool !== "comment")
@@ -130,7 +132,11 @@ const SideBarRight: React.FC = () => {
}
}
if (subModule === "properties" && activeModule !== "visualization") {
if (
subModule === "properties" &&
activeModule !== "visualization" &&
activeTool !== "comment"
) {
if (selectedAssets.length === 1) {
setDisplayComponent("assetProperties");
return;
@@ -230,12 +236,8 @@ const SideBarRight: React.FC = () => {
setDisplayComponent("globalProperties");
return;
}
if (activeModule === "builder" && activeTool === "comment") {
setDisplayComponent("threadDetails");
}
}
setDisplayComponent("none");
// setDisplayComponent("none");
}, [
viewVersionHistory,
activeModule,
@@ -287,8 +289,8 @@ const SideBarRight: React.FC = () => {
return <Visualization />;
case "resourceManagement":
return <ResourceManagement />;
case "threadDetails":
return <ThreadDetails />;
case "threadProperties":
return <ThreadProperties />;
default:
return null;
}

View File

@@ -0,0 +1,124 @@
import { useState } from "react";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { getRelativeTime } from "../../../ui/collaboration/function/getRelativeTime";
import { KebabIcon } from "../../../icons/ExportCommonIcons";
import { getAvatarColorUsingUserID } from "../../../../modules/collaboration/functions/getAvatarColor";
import { deleteThreadApi } from "../../../../services/builder/collab/comments/deleteThreadApi";
import { useParams } from "react-router-dom";
import { useSocketStore } from "../../../../store/socket/useSocketStore";
import { getUserData } from "../../../../functions/getUserData";
import useThreadResponseHandler from "../../../../modules/collaboration/responseHandler/useThreadResponseHandler";
const ThreadProperties = () => {
const { threadStore, versionStore } = useSceneContext();
const { threads, setSelectedThread } = threadStore();
const { userId, organization } = getUserData();
const { selectedVersion } = versionStore();
const { projectId } = useParams();
const { threadSocket } = useSocketStore();
const [selectedChart, setSelectedChart] = useState<ThreadSchema | null>(null);
const [isKebabActive, setIsKebabActive] = useState<boolean>(false);
const { removeThreadFromScene } = useThreadResponseHandler();
const handleDeleteThread = (val: ThreadSchema) => {
if (!projectId) return;
const threadIdToDelete = val?.threadId;
if (!threadSocket?.connected) {
// API fallback
deleteThreadApi(projectId, threadIdToDelete, selectedVersion?.versionId || "").then(
(res) => {
if (res.message === "Thread deleted Successfully") {
removeThreadFromScene(res.data._id);
}
}
);
} else {
// SOCKET path
const deleteThreadPayload = {
projectId,
userId,
organization,
threadId: threadIdToDelete,
versionId: selectedVersion?.versionId || "",
};
threadSocket.emit("v1:thread:delete", deleteThreadPayload);
}
};
return (
<div className="global-properties-container">
{threads &&
threads.map((val) => {
const isMenuVisible =
selectedChart?.createdAt === val.createdAt && isKebabActive;
return (
<section key={val?.threadId} className="thread-section">
<div className="thread-card">
{/* Avatar */}
<div
className="thread-avatar"
style={{
backgroundColor: getAvatarColorUsingUserID(val.creatorId),
}}
>
{val?.creatorName?.charAt(0).toUpperCase()}
</div>
{/* Content */}
<div className="thread-content">
{/* Title + username */}
<div>
<div className="thread-title">{val.threadTitle}</div>
<div className="thread-username">{val.creatorName}</div>
</div>
{/* Last updated + kebab */}
<div className="thread-footer">
<div className="thread-time">
{getRelativeTime(val.createdAt)}
</div>
<button
className="thread-kebab-button"
onClick={() => {
// Toggle menu visibility per-thread
if (
selectedChart?.createdAt === val.createdAt &&
isKebabActive
) {
setIsKebabActive(false);
setSelectedChart(null);
} else {
setSelectedChart(val);
setIsKebabActive(true);
}
}}
>
<KebabIcon />
</button>
</div>
</div>
</div>
{/* Kebab menu shown only for selected thread */}
{isMenuVisible && (
<div className="thread-menu">
<button onClick={() => setSelectedThread(val)}>Visible</button>
<button>Show in Scene</button>
{val.creatorId === userId && (
<button onClick={() => handleDeleteThread(val)}>
Delete
</button>
)}
</div>
)}
</section>
);
})}
</div>
);
};
export default ThreadProperties;

View File

@@ -1,7 +0,0 @@
import React from "react";
const ThreadDetails = () => {
return <div></div>;
};
export default ThreadDetails;

View File

@@ -1,22 +1,39 @@
import React, { useState } from "react";
import { getAvatarColor } from "../../../../modules/collaboration/functions/getAvatarColor";
import { getUserData } from "../../../../functions/getUserData";
import React, { useMemo, useState } from "react";
import {
// getAvatarColor,
getAvatarColorUsingUserID,
} from "../../../../modules/collaboration/functions/getAvatarColor";
import { getRelativeTime } from "../function/getRelativeTime";
interface CommentThreadsProps {
commentClicked: () => void;
thread?: ThreadSchema;
selectedThread: ThreadSchema | null;
}
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, thread }) => {
const CommentThreads = ({ commentClicked, thread, selectedThread }: CommentThreadsProps) => {
const [expand, setExpand] = useState(false);
const commentsedUsers = [{ creatorId: "1" }];
const { userName } = getUserData();
const commentedUsers = useMemo(() => {
if (!thread) return [];
function getUsername(userId: string) {
const UserName = userName?.charAt(0).toUpperCase() || "user";
return UserName;
}
const usersMap = new Map<string, { creatorId: string; creatorName: string }>();
usersMap.set(thread.creatorId, {
creatorId: thread.creatorId,
creatorName: thread.creatorName,
});
for (const reply of thread.comments) {
if (!usersMap.has(reply.creatorId)) {
usersMap.set(reply.creatorId, {
creatorId: reply.creatorId,
creatorName: reply.creatorName ?? "Unknown User",
});
}
}
return Array.from(usersMap.values());
}, [thread]);
function getDetails(type?: "clicked") {
if (type === "clicked") {
@@ -27,36 +44,49 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, thread
}
}
if (!thread) return null;
return (
<div className="comments-threads-wrapper">
<button
onPointerEnter={() => getDetails()}
onPointerEnter={() => {
getDetails();
}}
onPointerLeave={() => getDetails()}
onClick={() => getDetails("clicked")}
className={`comments-threads-container ${expand ? "open" : "closed"} unread`}
className={`comments-threads-container ${
expand || (selectedThread && selectedThread.threadId === thread.threadId)
? "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>
))}
{thread?.creatorId &&
commentedUsers.map((val, i) => (
<div
className="users"
key={val.creatorId}
style={{
// background: getAvatarColor(i, getUsername(val.creatorId)),
background: getAvatarColorUsingUserID(val.creatorId),
}}
>
{val.creatorName.charAt(0)}
</div>
))}
</div>
<div className={`last-comment-details ${expand ? "expand" : ""}`}>
<div className="header">
<div className="user-name">{userName}</div>
<div className="time">{thread?.createdAt && getRelativeTime(thread.createdAt)}</div>
<div className="user-name">{thread.creatorName}</div>
<div className="time">
{thread?.createdAt && getRelativeTime(thread.createdAt)}
</div>
</div>
<div className="message">{thread?.threadTitle}</div>
{thread && thread?.comments.length > 0 && (
<div className="comments">
{thread?.comments.length} {thread?.comments.length === 1 ? "comment" : "replies"}
{thread?.comments.length}{" "}
{thread?.comments.length === 1 ? "comment" : "replies"}
</div>
)}
</div>

View File

@@ -3,20 +3,21 @@ import { useParams } from "react-router-dom";
import { KebabIcon } from "../../../icons/ExportCommonIcons";
import { adjustHeight } from "../function/textAreaHeightAdjust";
import { useSocketStore } from "../../../../store/socket/useSocketStore";
import { useSelectedComment } from "../../../../store/builder/store";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { getAvatarColor } from "../../../../modules/collaboration/functions/getAvatarColor";
import {
getAvatarColor,
getAvatarColorUsingUserID,
} from "../../../../modules/collaboration/functions/getAvatarColor";
import { getUserData } from "../../../../functions/getUserData";
import { getRelativeTime } from "../function/getRelativeTime";
import { editThreadTitleApi } from "../../../../services/builder/collab/comments/editThreadTitleApi";
import { deleteCommentApi } from "../../../../services/builder/collab/comments/deleteCommentApi";
import { editCommentsApi } from "../../../../services/builder/collab/comments/editCommentApi";
import useThreadResponseHandler from "../../../../modules/collaboration/responseHandler/useThreadResponseHandler";
import { addCommentsApi } from "../../../../services/builder/collab/comments/addCommentApi";
interface MessageProps {
val: Reply | ThreadSchema;
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>>;
@@ -26,17 +27,25 @@ interface MessageProps {
mode?: "create" | "edit" | null;
}
const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEditable, setEditedThread, editedThread, isEditableThread, setMode }) => {
const Messages: React.FC<MessageProps> = ({
val,
mode,
setIsEditable,
setEditedThread,
editedThread,
isEditableThread,
setMode,
}) => {
const [openOptions, setOpenOptions] = useState(false);
const { projectId } = useParams();
const { threadSocket } = useSocketStore();
const { userName, userId, organization } = getUserData();
const { userId, organization } = getUserData();
const [isEditComment, setIsEditComment] = useState(false);
const { selectedComment, setCommentPositionState } = useSelectedComment();
const { versionStore, threadStore } = useSceneContext();
const { updateThread, removeReply, updateReply } = threadStore();
const { setCommentPositionState, selectedThread } = threadStore();
const { selectedVersion } = versionStore();
const { updateThreadInScene, removeReplyFromThread, updateReplyInThread } =
useThreadResponseHandler();
const [value, setValue] = useState<string>("comment" in val ? val.comment : val.threadTitle);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -52,26 +61,31 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
}
const handleSaveAction = async () => {
if (!projectId || !selectedVersion) return;
if (!projectId || !selectedVersion || !selectedThread) return;
console.log("mode: ", mode);
if (isEditableThread && editedThread) {
if (!threadSocket?.active) {
// API
editThreadTitleApi(projectId, (val as ThreadSchema).threadId, value, selectedVersion?.versionId || "").then((editThreadTitle) => {
editThreadTitleApi(
projectId,
(val as ThreadSchema).threadId,
value,
selectedVersion?.versionId || ""
).then((editThreadTitle) => {
if (editThreadTitle.message == "ThreadTitle updated Successfully") {
const editedThread: ThreadSchema = {
state: "active",
threadId: editThreadTitle.data.replyId,
creatorId: userId,
const updatedThread: ThreadSchema = {
state: editThreadTitle.data.state,
threadId: editThreadTitle.data._id,
creatorId: editThreadTitle.data.createdBy._id,
creatorName: editThreadTitle.data.createdBy.userName,
threadTitle: editThreadTitle.data.threadTitle,
createdAt: getRelativeTime(editThreadTitle.data.createdAt),
threadTitle: value,
lastUpdatedAt: new Date().toISOString(),
position: editThreadTitle.data.position,
rotation: [0, 0, 0],
comments: [],
rotation: editThreadTitle.data.rotation,
comments: editThreadTitle.data.comments,
};
updateThread((val as ThreadSchema).threadId, editedThread);
updateThreadInScene(editThreadTitle.data._id, updatedThread);
}
});
} else {
@@ -82,7 +96,7 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
userId,
threadTitle: value,
organization,
threadId: (val as ThreadSchema).threadId || selectedComment.threadId,
threadId: (val as ThreadSchema).threadId || selectedThread.threadId,
versionId: selectedVersion?.versionId || "",
};
@@ -91,17 +105,23 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
} else if (mode === "edit") {
if (!threadSocket?.active) {
// API
editCommentsApi(projectId, value, selectedComment?.threadId, (val as Reply).replyId, selectedVersion?.versionId || "").then((editComments) => {
const commentData = {
replyId: `${editComments.data?.replyId}`,
creatorId: `${userId}`,
createdAt: getRelativeTime(editComments.data?.createdAt),
addCommentsApi(
projectId,
value,
selectedThread?.threadId,
selectedVersion?.versionId || "",
(val as Reply).replyId
).then((editedComment) => {
console.log("editedComment: ", editedComment);
const commentData: Partial<Reply> = {
lastUpdatedAt: "2 hrs ago",
comment: value,
comment: editedComment.data.comment,
};
updateReply((val as ThreadSchema).threadId, (val as Reply)?.replyId, commentData);
updateReplyInThread(
selectedThread.threadId,
editedComment.data._id,
commentData
);
if (setIsEditable) setIsEditable(true);
if (setEditedThread) setEditedThread(false);
@@ -114,7 +134,7 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
userId,
comment: value,
organization,
threadId: selectedComment?.threadId,
threadId: selectedThread?.threadId,
commentId: (val as Reply).replyId ?? "",
versionId: selectedVersion?.versionId || "",
};
@@ -128,16 +148,24 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
};
const handleDeleteAction = async (replyID: any) => {
if (!projectId || !selectedVersion || !setMessages) return;
if (!projectId || !selectedVersion || !selectedThread) return;
setOpenOptions(false);
if (!threadSocket?.active) {
// API
deleteCommentApi(projectId, selectedComment?.threadId, (val as Reply).replyId, selectedVersion?.versionId || "").then((deletedComment) => {
deleteCommentApi(
projectId,
selectedThread?.threadId,
(val as Reply).replyId,
selectedVersion?.versionId || ""
).then((deletedComment) => {
if (deletedComment === "'Thread comment deleted Successfully'") {
setMessages((prev) => prev.filter((message) => message.replyId !== replyID));
removeReply(val.creatorId, replyID);
removeReplyFromThread(
deletedComment.data._id,
deletedComment.data.comments[0]._id
);
console.log(deletedComment.data._id, deletedComment.data.comments[0]._id);
}
});
} else {
@@ -148,13 +176,9 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
userId,
commentId: (val as Reply).replyId,
organization,
threadId: selectedComment?.threadId,
threadId: selectedThread?.threadId,
versionId: selectedVersion?.versionId || "",
};
setMessages((prev) => {
return prev.filter((message) => message.replyId !== (val as Reply).replyId);
});
removeReply(selectedComment?.threadId, (val as Reply).replyId);
threadSocket.emit("v1-Comment:delete", deleteComment);
}
@@ -174,7 +198,15 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
{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} />
<textarea
placeholder="type here"
ref={textareaRef}
autoFocus
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ resize: "none" }}
onFocus={handleFocus}
/>
</div>
<div className="actions-container">
<div className="options"></div>
@@ -200,16 +232,24 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
</div>
) : (
<div className="message-container">
<div className="profile" style={{ background: getAvatarColor(i, userId) }}>
{userName?.charAt(0).toUpperCase() || "user"}
<div
className="profile"
style={{ background: getAvatarColorUsingUserID(val.creatorId) }}
>
{val.creatorName?.charAt(0).toUpperCase() || "U"}
</div>
<div className="content">
<div className="user-details">
<div className="user-name">{userName}</div>
<div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div>
<div className="user-name">{val.creatorName}</div>
<div className="time">
{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}
</div>
</div>
{(val as Reply).creatorId === userId && (
<div className="more-options" onMouseLeave={() => setOpenOptions(false)}>
<div
className="more-options"
onMouseLeave={() => setOpenOptions(false)}
>
<button
className="more-options-button"
onClick={() => {
@@ -249,7 +289,9 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
</div>
)}
<div className="message">{"comment" in val ? val.comment : val.threadTitle}</div>
<div className="message">
{"comment" in val ? val.comment : val.threadTitle}
</div>
</div>
</div>
)}

View File

@@ -3,64 +3,65 @@ import { useParams } from "react-router-dom";
import { CloseIcon, KebabIcon } from "../../../icons/ExportCommonIcons";
import { ExpandIcon } from "../../../icons/SimulationIcons";
import { adjustHeight } from "../function/textAreaHeightAdjust";
import { useSelectedComment } from "../../../../store/builder/store";
import { useSocketStore } from "../../../../store/socket/useSocketStore";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import Messages from "./Messages";
import ThreadSocketResponsesDev from "../../../../modules/collaboration/socket/threadSocketResponses.dev";
import { getUserData } from "../../../../functions/getUserData";
import { addCommentsApi } from "../../../../services/builder/collab/comments/addCommentApi";
import { deleteThreadApi } from "../../../../services/builder/collab/comments/deleteThreadApi";
import { createThreadApi } from "../../../../services/builder/collab/comments/createThreadApi";
import { getRelativeTime } from "../function/getRelativeTime";
import useThreadResponseHandler from "../../../../modules/collaboration/responseHandler/useThreadResponseHandler";
const ThreadChat: React.FC = () => {
const { userId, organization } = getUserData();
const [openThreadOptions, setOpenThreadOptions] = useState(false);
const [inputActive, setInputActive] = useState(false);
const [value, setValue] = useState<string>("");
const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState, position2Dstate } = useSelectedComment();
const [mode, setMode] = useState<"create" | "edit" | null>("create");
const [isEditable, setIsEditable] = useState(false);
const [editedThread, setEditedThread] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<Reply[]>([]);
const { projectId } = useParams();
const [dragging, setDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState({ x: position2Dstate.x, y: position2Dstate.y });
const { threadSocket } = useSocketStore();
const modeRef = useRef<"create" | "edit" | null>(null);
const messagesRef = useRef<HTMLDivElement>(null);
const { versionStore, threadStore } = useSceneContext();
const { selectedVersion } = versionStore();
const { addThread, removeThread, addReply, threads } = threadStore();
const {
threads,
getThreadById,
selectedThread,
setSelectedThread,
setCommentPositionState,
commentPositionState,
position2Dstate,
} = threadStore();
const [position, setPosition] = useState<{ x?: number; y?: number }>({
x: (position2Dstate as any)?.x,
y: (position2Dstate as any)?.y,
});
const { addThreadToScene, addReplyToThread, removeThreadFromScene } =
useThreadResponseHandler();
useEffect(() => {
if (threads.length > 0 && selectedThread) {
const thread = getThreadById(selectedThread.threadId);
if (thread) {
setSelectedThread(thread);
}
}
console.log("threads: ", threads);
}, [threads, selectedThread]);
useEffect(() => {
modeRef.current = mode;
}, [mode]);
useEffect(() => {
if (threads.length > 0 && selectedComment) {
const allMessages = threads
.flatMap((val: any) => (val?.threadId === selectedComment?.threadId ? val.comments : []))
.map((c) => {
return {
replyId: c._id ?? c.replyId,
creatorId: c.creatorId || c.userId,
createdAt: c.createdAt,
lastUpdatedAt: "1 hr ago",
comment: c.comment,
_id: c._id ?? c.replyId,
};
});
setMessages(allMessages);
}
}, [selectedComment]);
useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current);
}, [value]);
@@ -97,7 +98,10 @@ const ThreadChat: React.FC = () => {
wrapper.setPointerCapture(event.pointerId);
};
const updatePosition = ({ clientX, clientY }: { clientX: number; clientY: number }, allowMove: boolean = true) => {
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");
@@ -125,7 +129,7 @@ const ThreadChat: React.FC = () => {
// Commented this useEffect to prevent offset after user saved the comment
// useEffect(() => {
// updatePosition({ clientX: position.x, clientY: position.y }, true);
// }, [selectedComment]);
// }, [selectedThread]);
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return;
@@ -136,51 +140,62 @@ const ThreadChat: React.FC = () => {
const handleCreateComments = async (e: any) => {
e.preventDefault();
if (!value) return;
if (!value || !selectedThread) return;
if (!threadSocket?.connected && mode === "create") {
if (!threadSocket?.connected) {
// API
addCommentsApi(projectId, value, selectedComment?.threadId, selectedVersion?.versionId || "").then((createComments) => {
if (createComments.message === "Thread comments add Successfully" && createComments.data) {
addCommentsApi(
projectId,
value,
selectedThread?.threadId,
selectedVersion?.versionId || ""
).then((createComments) => {
console.log("createComments: ", createComments);
if (
createComments?.message === "Thread comments add Successfully" &&
createComments.data
) {
const commentData = {
replyId: `${createComments.data?._id}`,
creatorId: `${selectedComment?.threadId}`,
createdAt: "2 hrs ago",
creatorId: `${createComments.data?.userId}`,
createdAt: getRelativeTime(createComments.data?.createdAt),
lastUpdatedAt: "2 hrs ago",
comment: value,
creatorName: createComments.data?.creatorName,
comment: createComments.data?.comment,
};
setMessages((prevMessages) => [...prevMessages, commentData]);
addReply(selectedComment?.threadId, commentData);
addReplyToThread(createComments?.data.threadId[0], commentData);
}
});
} else if (threadSocket?.connected && mode === "create") {
} else if (threadSocket?.connected) {
// SOCKET
console.log("else works");
const addThread = {
versionId: selectedVersion?.versionId || "",
projectId,
userId,
comment: value,
organization,
threadId: selectedComment?.threadId,
threadId: selectedThread?.threadId,
};
console.log("addThread: ", addThread);
threadSocket.emit("v1-Comment:add", addThread);
}
setInputActive(false);
};
const handleDeleteThread = async () => {
if (!projectId) return;
if (!projectId || !selectedThread) return;
if (!threadSocket?.connected) {
// API
deleteThreadApi(projectId, selectedComment?.threadId, selectedVersion?.versionId || "").then((deleteThread) => {
deleteThreadApi(
projectId,
selectedThread?.threadId,
selectedVersion?.versionId || ""
).then((deleteThread) => {
if (deleteThread.message === "Thread deleted Successfully") {
removeThread(selectedComment?.threadId);
setSelectedComment(null);
removeThreadFromScene(deleteThread.data._id);
setSelectedThread(null);
}
});
} else {
@@ -190,11 +205,10 @@ const ThreadChat: React.FC = () => {
projectId,
userId,
organization,
threadId: selectedComment?.threadId,
threadId: selectedThread?.threadId,
versionId: selectedVersion?.versionId || "",
};
setSelectedComment(null);
removeThread(selectedComment?.threadId);
setSelectedThread(null);
threadSocket.emit("v1:thread:delete", deleteThread);
}
};
@@ -205,24 +219,31 @@ const ThreadChat: React.FC = () => {
if (!threadSocket?.connected) {
// API
createThreadApi(projectId, "active", commentPositionState.position, [0, 0, 0], value, selectedVersion?.versionId || "").then((thread) => {
createThreadApi(
projectId,
"active",
(commentPositionState as any)?.position,
[0, 0, 0],
value,
selectedVersion?.versionId || ""
).then((thread) => {
if (thread.message === "Thread created Successfully" && thread?.threadData) {
const comment: ThreadSchema = {
state: "active",
state: thread.threadData.state,
threadId: thread?.threadData?._id,
creatorId: userId,
creatorId: thread.threadData.createdBy,
creatorName: thread?.threadData.creatorName,
createdAt: getRelativeTime(thread.threadData?.createdAt),
threadTitle: value,
threadTitle: thread.threadData.threadTitle,
lastUpdatedAt: new Date().toISOString(),
position: commentPositionState.position,
rotation: [0, 0, 0],
position: thread.threadData.position,
rotation: thread.threadData.rotation,
comments: [],
};
addThread(comment);
addThreadToScene(comment);
setCommentPositionState(null);
setInputActive(false);
setSelectedComment(null);
setSelectedThread(null);
}
});
} else {
@@ -234,15 +255,17 @@ const ThreadChat: React.FC = () => {
userId,
organization,
state: "active",
position: commentPositionState.position,
position: (commentPositionState as any)?.position,
rotation: [0, 0, 0],
threadTitle: value,
};
setCommentPositionState(null);
setInputActive(false);
setSelectedComment(null);
setSelectedThread(null);
threadSocket.emit("v1:thread:create", createThread);
// Listen for response
}
};
@@ -254,8 +277,8 @@ const ThreadChat: React.FC = () => {
};
useEffect(() => {
if (messages.length > 0) scrollToBottom();
}, [messages]);
if (selectedThread && selectedThread.comments.length > 0) scrollToBottom();
}, [selectedThread]);
return (
<div
@@ -291,15 +314,17 @@ const ThreadChat: React.FC = () => {
<div className="options-list">
<div className="options">Mark as Unread</div>
<div className="options">Mark as Resolved</div>
<div className="options delete" onClick={handleDeleteThread}>
Delete Thread
</div>
{selectedThread?.creatorId === userId && (
<div className="options delete" onClick={handleDeleteThread}>
Delete Thread
</div>
)}
</div>
)}
<button
className="close-button"
onClick={() => {
setSelectedComment(null);
setSelectedThread(null);
setCommentPositionState(null);
}}
>
@@ -309,13 +334,19 @@ const ThreadChat: React.FC = () => {
</div>
<div className="messages-wrapper" ref={messagesRef}>
{selectedComment && <Messages val={selectedComment} i={1} key={selectedComment.creatorId} isEditableThread={true} setEditedThread={setEditedThread} editedThread={editedThread} />}
{messages.map((val, i) => (
{selectedThread && (
<Messages
val={selectedThread}
key={selectedThread?.creatorId}
isEditableThread={true}
setEditedThread={setEditedThread}
editedThread={editedThread}
/>
)}
{(selectedThread as ThreadSchema)?.comments.map((val, i) => (
<Messages
val={val as any}
i={i}
key={val.replyId}
setMessages={setMessages}
setIsEditable={setIsEditable}
isEditable={isEditable}
isEditableThread={false}
@@ -328,14 +359,18 @@ const ThreadChat: React.FC = () => {
<div className="send-message-wrapper">
<div className={`input-container ${inputActive ? "active" : ""}`}>
<textarea
placeholder={commentPositionState && selectedComment === null ? "Type Thread Title" : "type something"}
placeholder={
commentPositionState && selectedThread === null
? "Type Thread Title"
: "type something"
}
ref={textareaRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (commentPositionState && selectedComment === null) {
if (commentPositionState && selectedThread === null) {
handleCreateThread(e);
} else {
setMode("create");
@@ -349,11 +384,11 @@ const ThreadChat: React.FC = () => {
}
}}
onClick={() => {
if (!commentPositionState && selectedComment !== null) {
if (!commentPositionState && selectedThread !== null) {
setMode("create");
}
}}
autoFocus={selectedComment === null}
autoFocus={selectedThread === null}
onBlur={() => setInputActive(false)}
onFocus={() => setInputActive(true)}
style={{ resize: "none" }}
@@ -361,7 +396,7 @@ const ThreadChat: React.FC = () => {
<div
className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}
onClick={(e) => {
if (commentPositionState && selectedComment === null) {
if (commentPositionState && selectedThread === null) {
handleCreateThread(e);
} else {
setMode("create");
@@ -375,7 +410,6 @@ const ThreadChat: React.FC = () => {
</div>
</div>
</div>
<ThreadSocketResponsesDev setMessages={setMessages} modeRef={modeRef} messages={messages} />
</div>
);
};

View File

@@ -36,12 +36,13 @@ export function getAvatarColor(index: number, userId?: string): string {
// Find a new color not already assigned
const usedColors = Object.values(userColors);
const availableColors = avatarColors.filter(color => !usedColors.includes(color));
const availableColors = avatarColors.filter((color) => !usedColors.includes(color));
// Assign a new color
const assignedColor = availableColors.length > 0
? availableColors[0]
: avatarColors[index % avatarColors.length];
const assignedColor =
availableColors.length > 0
? availableColors[0]
: avatarColors[index % avatarColors.length];
userColors[userId] = assignedColor;
@@ -54,3 +55,12 @@ export function getAvatarColor(index: number, userId?: string): string {
// Fallback: Assign a color using the index if no userId or local storage is unavailable
return avatarColors[index % avatarColors.length];
}
export function getAvatarColorUsingUserID(userId: string): string {
// Simple deterministic color based on userId hash
let hash = 1;
for (let i = 1; i < userId.length; i++) {
hash = userId.charCodeAt(i) + ((hash << 5) - hash);
}
hash = Math.abs(hash);
return avatarColors[hash % avatarColors.length];
}

View File

@@ -0,0 +1,67 @@
import { useCallback } from "react";
import { useSceneContext } from "../../scene/sceneContext";
function useThreadResponseHandler() {
const { threadStore } = useSceneContext();
const { addThread, updateThread, removeThread, addReply, updateReply, removeReply } =
threadStore();
const addThreadToScene = useCallback(
(thread: ThreadSchema, callback?: () => void) => {
addThread(thread);
callback?.();
},
[addThread]
);
const updateThreadInScene = useCallback(
(threadId: string, updates: Partial<ThreadSchema>, callback?: () => void) => {
updateThread(threadId, updates);
callback?.();
},
[updateThread]
);
const removeThreadFromScene = useCallback(
(threadId: string, callback?: () => void) => {
removeThread(threadId);
callback?.();
},
[removeThread]
);
const addReplyToThread = useCallback(
(threadId: string, reply: Reply, callback?: () => void) => {
addReply(threadId, reply);
callback?.();
},
[addReply]
);
const updateReplyInThread = useCallback(
(threadId: string, replyId: string, updates: Partial<Reply>, callback?: () => void) => {
updateReply(threadId, replyId, updates);
callback?.();
},
[updateReply]
);
const removeReplyFromThread = useCallback(
(threadId: string, replyId: string, callback?: () => void) => {
removeReply(threadId, replyId);
callback?.();
},
[removeReply]
);
return {
addThreadToScene,
updateThreadInScene,
removeThreadFromScene,
addReplyToThread,
updateReplyInThread,
removeReplyFromThread,
};
}
export default useThreadResponseHandler;

View File

@@ -0,0 +1,122 @@
import { useEffect } from "react";
import { useSocketStore } from "../../../store/socket/useSocketStore";
import { getRelativeTime } from "../../../components/ui/collaboration/function/getRelativeTime";
import useThreadResponseHandler from "../responseHandler/useThreadResponseHandler";
const CollaborationResponses = () => {
const { threadSocket } = useSocketStore();
const {
addThreadToScene,
updateThreadInScene,
removeThreadFromScene,
addReplyToThread,
removeReplyFromThread,
} = useThreadResponseHandler();
//#region Thread
useEffect(() => {
if (!threadSocket) return;
threadSocket.on("v1-thread:response:create", (data: any) => {
if (!data.message || !data.data) {
echo.error(`Error adding or updating thread`);
return;
}
if (data.message === "Thread created Successfully") {
const comment: ThreadSchema = {
state: data.data.state,
threadId: data.data._id,
creatorId: data.data.createdBy,
creatorName: data.data.creatorName,
threadTitle: data.data.threadTitle,
createdAt: getRelativeTime(data.data.createdAt),
position: data.data.position,
rotation: data.data.rotation,
comments: [],
};
addThreadToScene(comment, () => {
echo.log("Thread created successfully");
});
}
});
threadSocket.on("v1-thread:response:delete", (data: any) => {
if (!data.message || !data.data) {
echo.error(`Error in deleting thread`);
return;
}
if (data.message === "Thread deleted Successfully") {
removeThreadFromScene(data.data._id, () => {
echo.log("Thread Deleted successfully");
});
}
});
threadSocket.on("v1-thread:response:updateTitle", (data: any) => {
if (!data.message || !data.data) {
echo.error(`Error updating thread`);
return;
}
if (data.message === "ThreadTitle updated Successfully") {
const updatedThread: ThreadSchema = {
state: data.data.state,
threadId: data.data._id,
creatorId: data.data.createdBy._id,
creatorName: data.data.createdBy.userName,
threadTitle: data.data.threadTitle,
createdAt: getRelativeTime(data.data.createdAt),
position: data.data.position,
rotation: data.data.rotation,
comments: data.data.comments,
};
updateThreadInScene(data.data._id, updatedThread, () => {
echo.log("Thread title is updated");
});
}
});
threadSocket.on("v1-Comment:response:add", (data: any) => {
if (data.message === "Thread comments add Successfully") {
const reply: Reply = {
creatorName: data.data.creatorName,
creatorId: data.data.userId,
createdAt: getRelativeTime(data.data.createdAt),
comment: data.data.comment,
lastUpdatedAt: "2hrs",
replyId: data.data._id,
};
addReplyToThread(data.data.threadId[0], reply);
}
});
threadSocket.on("v1-Comment:response:delete", (data: any) => {
if (!data.message || !data.data) {
echo.error(`Error deleting reply`);
return;
}
if (data.message === "Thread comment deleted Successfully") {
removeReplyFromThread(data.data._id, data.data.comments[0]._id, () => {
echo.log("Reply added Successfully");
});
}
});
return () => {
if (threadSocket) {
threadSocket.off("v1-thread:response:create");
threadSocket.off("v1-thread:response:delete");
threadSocket.off("v1-thread:response:updateTitle");
threadSocket.off("v1-Comment:response:add");
threadSocket.off("v1-Comment:response:delete");
}
};
}, [threadSocket]);
//#endregion
return null;
};
export default CollaborationResponses;

View File

@@ -1,6 +1,7 @@
import UserResponses from "./userResponses";
import BuilderResponses from "./builderResponses";
import SimulationResponses from "./simulationResponses";
import CollaborationResponses from "./collaborationResponses";
export default function SocketResponses() {
return (
@@ -10,6 +11,8 @@ export default function SocketResponses() {
<BuilderResponses />
<SimulationResponses />
<CollaborationResponses />
</>
);
}

View File

@@ -1,164 +0,0 @@
import React, { useEffect } from "react";
import { useSelectedComment } from "../../../store/builder/store";
import { useSocketStore } from "../../../store/socket/useSocketStore";
import { getUserData } from "../../../functions/getUserData";
import { getRelativeTime } from "../../../components/ui/collaboration/function/getRelativeTime";
import { useSceneContext } from "../../scene/sceneContext";
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 { threadStore } = useSceneContext();
const { threads, removeReply, addThread, addReply, updateThread, updateReply } = threadStore();
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) => {
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;
})
);
}
};
threadSocket.on("v1-Comment:response:add", handleAddComment);
// --- Delete Comment Handler ---
const handleDeleteComment = (data: any) => {};
threadSocket.on("v1-Comment:response:delete", handleDeleteComment);
// --- Create Thread Handler ---
const handleCreateThread = (data: any) => {
const comment: ThreadSchema = {
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);
addThread(comment);
setCommentPositionState(null);
// setSelectedComment(null);
};
threadSocket.on("v1-thread:response:create", handleCreateThread);
// --- Delete Thread Handler ---
// const handleDeleteThread = (data: any) => {
//
// };
// threadSocket.on("v1-thread:response:delete", handleDeleteThread);
const handleDeleteThread = (data: any) => {};
threadSocket.on("v1-thread:response:delete", handleDeleteThread);
const handleEditThread = (data: any) => {
const editedThread: ThreadSchema = {
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,
};
//
updateThread(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,
setSelectedComment,
setCommentPositionState,
threads,
modeRef.current,
messages,
]);
return null;
};
export default ThreadSocketResponsesDev;

View File

@@ -3,38 +3,40 @@ import { useThree } from "@react-three/fiber";
import { Html, TransformControls } from "@react-three/drei";
import { Group, Object3D, Vector3 } from "three";
import { usePlayButtonStore } from "../../../../../store/ui/usePlayButtonStore";
import { useSelectedComment } from "../../../../../store/builder/store";
import CommentThreads from "../../../../../components/ui/collaboration/threads/CommentThreads";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { useSceneContext } from "../../../../scene/sceneContext";
function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
const { threadStore } = useSceneContext();
const { selectedThread, setSelectedThread, setPosition2Dstate } = threadStore();
const { isPlaying } = usePlayButtonStore();
const CommentRef = useRef(null);
const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
const { selectedComment, setSelectedComment, setPosition2Dstate } = useSelectedComment();
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(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const keyCombination = detectModifierKeys(e);
if (!selectedThread) 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]);
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedThread]);
const commentClicked = () => {
setSelectedComment(thread);
setSelectedThread(thread);
const position = new Vector3(thread.position[0], thread.position[1], thread.position[2]);
position.project(camera);
@@ -48,10 +50,10 @@ function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
};
useEffect(() => {
if (!selectedComment || selectedComment.threadId !== thread.threadId) {
if (!selectedThread || selectedThread.threadId !== thread.threadId) {
setSelectedObject(null);
}
}, [selectedComment]);
}, [selectedThread]);
if (thread.state === "inactive" || isPlaying) return null;
@@ -68,10 +70,14 @@ function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
// rotation={comment.rotation}
className="comments-main-wrapper"
>
<CommentThreads commentClicked={commentClicked} thread={thread} />
<CommentThreads
commentClicked={commentClicked}
thread={thread}
selectedThread={selectedThread}
/>
</Html>
</group>
{/* {selectedObject && transformMode && (
{selectedObject && transformMode && (
<TransformControls
object={selectedObject}
mode={transformMode}
@@ -79,7 +85,7 @@ function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
console.log("sad");
}}
/>
)} */}
)}
</>
);
}

View File

@@ -8,26 +8,25 @@ import { useSceneContext } from "../../../scene/sceneContext";
function ThreadInstances() {
const { projectId } = useParams();
const { userId } = getUserData();
const { versionStore, threadStore } = useSceneContext();
const { threads, setThreads } = threadStore();
const { selectedVersion } = versionStore();
useEffect(() => {
// console.log("threads", threads);
}, [threads]);
useEffect(() => {
if (!projectId || !selectedVersion) return;
getAllThreads(projectId, selectedVersion?.versionId)
.then((fetchedComments) => {
const formattedThreads = Array.isArray(fetchedComments.data)
const formattedThreads = Array.isArray(fetchedComments?.data)
? fetchedComments.data.map((thread: any) => ({
...thread,
creatorId: thread.creatorId._id,
creatorName: thread.creatorId.userName,
comments: Array.isArray(thread.comments)
? thread.comments.map((val: any) => ({
replyId: val._id ?? "",
creatorId: userId,
creatorId: val.userId._id,
creatorName: val.userId.userName,
createdAt: getRelativeTime(val.createdAt),
lastUpdatedAt: "1 hr ago",
comment: val.comment,

View File

@@ -3,13 +3,15 @@ import { useThree } from "@react-three/fiber";
import { Vector3 } from "three";
import ThreadInstances from "./threadInstances/threadInstances";
import { Sphere } from "@react-three/drei";
import { useActiveTool, useSelectedComment } from "../../../store/builder/store";
import { useActiveTool } from "../../../store/builder/store";
import { useSceneContext } from "../../scene/sceneContext";
function ThreadsGroup() {
const { gl, raycaster, camera, scene, pointer, size } = useThree();
const { activeTool } = useActiveTool();
const [hoverPos, setHoverPos] = useState<Vector3 | null>(null);
const { setSelectedComment, setCommentPositionState, setPosition2Dstate } = useSelectedComment();
const { threadStore } = useSceneContext();
const { setSelectedThread, setCommentPositionState, setPosition2Dstate } = threadStore();
useEffect(() => {
const canvasElement = gl.domElement;
@@ -83,8 +85,12 @@ function ThreadsGroup() {
intersect.object.type !== "GridHelper"
);
if (intersects.length > 0) {
const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z);
setSelectedComment(null);
const position = new Vector3(
intersects[0].point.x,
Math.max(intersects[0].point.y, 0),
intersects[0].point.z
);
setSelectedThread(null);
setCommentPositionState({ position: position.toArray() });
position.project(camera);

View File

@@ -1,8 +1,14 @@
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) => {
export const addCommentsApi = async (
projectId: any,
comment: string,
threadId: string,
versionId: string,
commentId?: string
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/Thread/addThread`, {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/Thread/addComment`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
@@ -10,8 +16,16 @@ export const addCommentsApi = async (projectId: any, comment: string, threadId:
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectId, comment, threadId, versionId }),
body: JSON.stringify({ projectId, comment, threadId, versionId, commentId }),
});
console.log(
"projectId, comment, threadId, versionId: ",
projectId,
comment,
threadId,
versionId,
commentId
);
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
@@ -23,6 +37,7 @@ export const addCommentsApi = async (projectId: any, comment: string, threadId:
}
const result = await response.json();
console.log("result: ", result);
return result;
} catch {

View File

@@ -17,7 +17,14 @@ export const createThreadApi = async (
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectId, state, position, rotation, threadTitle, versionId }),
body: JSON.stringify({
projectId,
state,
position,
rotation,
threadTitle,
versionId,
}),
});
const newAccessToken = response.headers.get("x-access-token");

View File

@@ -31,6 +31,7 @@ export const deleteThreadApi = async (
}
const result = await response.json();
console.log('result: ', result);
return result;
} catch {
echo.error("Failed to delete thread");

View File

@@ -1,8 +1,14 @@
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) => {
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/addThread`, {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/Thread/addComment`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
@@ -23,6 +29,7 @@ export const editCommentsApi = async (projectId: any, comment: string, commentId
}
const result = await response.json();
console.log("result: ", result);
return result;
} catch {

View File

@@ -435,15 +435,6 @@ export const useCompareProductDataStore = create<{
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 }),
}));
export const useSelectedPath = create<any>((set: any) => ({
selectedPath: "auto",
setSelectedPath: (x: any) => set({ selectedPath: x }),

View File

@@ -1,8 +1,18 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
interface CommentPositionState {
position: [number, number, number];
}
interface ThreadStore {
threads: ThreadsSchema;
selectedThread: ThreadSchema | null;
position2Dstate: {};
setSelectedThread: (thread: ThreadSchema | null) => void;
setPosition2Dstate: (thread: {}) => void;
commentPositionState: null;
setCommentPositionState: (thread: CommentPositionState | null) => void;
addThread: (thread: ThreadSchema) => void;
setThreads: (threads: ThreadsSchema) => void;
@@ -21,6 +31,9 @@ export const createThreadsStore = () => {
return create<ThreadStore>()(
immer((set, get) => ({
threads: [],
selectedThread: null,
setSelectedThread: (thread) => set({ selectedThread: thread }),
addThread: (thread) => {
set((state) => {
@@ -35,6 +48,10 @@ export const createThreadsStore = () => {
state.threads = threads;
});
},
position2Dstate: {},
commentPositionState: null,
setPosition2Dstate: (x: any) => set({ position2Dstate: x }),
setCommentPositionState: (x: any) => set({ commentPositionState: x }),
updateThread: (threadId, updates) => {
set((state) => {

View File

@@ -74,7 +74,7 @@ export const useSocketStore = create<SocketStore>((set, get) => ({
if (get().threadSocket) return;
const threadSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`, socketOptions({ token, refreshToken, projectId }));
attachLogs("Thread", threadSocket);
set({ threadSocket });
set({ threadSocket })
},
initializeProjectSocket: (token, refreshToken) => {

View File

@@ -1503,12 +1503,24 @@
&.yellow-black {
background-color: black;
background-size: 10px 10px;
background-image: repeating-linear-gradient(45deg, #fbe50e 0, #fbe50e 2px, black 0, black 50%);
background-image: repeating-linear-gradient(
45deg,
#fbe50e 0,
#fbe50e 2px,
black 0,
black 50%
);
}
&.white-black {
background-color: black;
background-size: 10px 10px;
background-image: repeating-linear-gradient(45deg, white 0, white 2px, black 0, black 50%);
background-image: repeating-linear-gradient(
45deg,
white 0,
white 2px,
black 0,
black 50%
);
}
}
@@ -1781,6 +1793,99 @@
}
}
}
.thread-section {
margin-bottom: 12px;
}
.thread-card {
display: flex;
align-items: flex-start;
gap: 12px;
border: 1px solid #444;
border-radius: 16px;
padding: 12px 16px;
color: #fff;
font-family: "Inter", sans-serif;
}
.thread-avatar {
height: 36px;
width: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
font-size: 14px;
}
.thread-content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
height: 100%;
}
.thread-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 2px;
}
.thread-username {
font-size: 12px;
color: #aaa;
}
.thread-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.thread-time {
font-size: 11px;
color: #888;
}
.thread-kebab-button {
background: none;
border: none;
cursor: pointer;
color: #fff;
&:hover {
opacity: 0.8;
}
}
.thread-menu {
position: absolute;
height: 100px;
width: 150px;
background-color: #222;
border: 1px solid #444;
border-radius: 8px;
right: 0;
padding: 8px;
display: flex;
flex-direction: column;
justify-content: space-around;
color: #fff;
p {
margin: 0;
font-size: 13px;
cursor: pointer;
&:hover {
color: #00b4d8;
}
}
}
.toggle-sidebar-ui-button {
svg {
@@ -2099,7 +2204,11 @@
&:nth-child(2) {
&::after {
// @include gradient-by-child(4); // Second child uses the second color
background: linear-gradient(144.19deg, rgba(197, 137, 26, 0.5) 16.62%, rgba(69, 48, 10, 0.5) 85.81%);
background: linear-gradient(
144.19deg,
rgba(197, 137, 26, 0.5) 16.62%,
rgba(69, 48, 10, 0.5) 85.81%
);
}
}
@@ -2232,7 +2341,11 @@
width: 100%;
height: 100%;
font-size: var(--font-size-regular);
background: linear-gradient(0deg, rgba(37, 24, 51, 0) 0%, rgba(52, 41, 61, 0.5) 100%);
background: linear-gradient(
0deg,
rgba(37, 24, 51, 0) 0%,
rgba(52, 41, 61, 0.5) 100%
);
pointer-events: none;
backdrop-filter: blur(8px);
opacity: 0;

View File

@@ -21,8 +21,9 @@ interface ThreadSchema {
threadId: string;
creatorId: string;
createdAt: string;
creatorName: string;
threadTitle: string;
lastUpdatedAt: string;
lastUpdatedAt?: string;
position: [number, number, number];
rotation: [number, number, number];
comments: Reply[];
@@ -32,6 +33,7 @@ interface Reply {
replyId: string;
creatorId: string;
createdAt: string;
creatorName: string;
lastUpdatedAt: string;
comment: string;
}

View File

@@ -7,7 +7,6 @@ import {
useDfxUpload,
useRenameModeStore,
useIsComparing,
useSelectedComment,
useShortcutStore,
useToggleView,
useToolMode,
@@ -24,7 +23,7 @@ import { useSceneContext } from "../../modules/scene/sceneContext";
const KeyPressListener: React.FC = () => {
const { comparisonScene, clearComparisonState } = useSimulationState();
const { activeModule, setActiveModule } = useModuleStore();
const { assetStore, versionStore } = useSceneContext();
const { assetStore, versionStore, threadStore } = useSceneContext();
const { selectedAssets } = assetStore();
const { setSubModule } = useSubModuleStore();
const { setActiveSubTool } = useActiveSubTool();
@@ -43,9 +42,12 @@ const KeyPressListener: React.FC = () => {
const { isRenameMode, setIsRenameMode } = useRenameModeStore();
const { setSelectedWallAsset } = useBuilderStore();
const { setCreateNewVersion, setVersionHistoryVisible } = versionStore();
const { setSelectedComment } = useSelectedComment();
const { setSelectedThread } = threadStore();
const { setDfxUploaded } = useDfxUpload();
const isTextInput = (element: Element | null): boolean => element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element?.getAttribute("contenteditable") === "true";
const isTextInput = (element: Element | null): boolean =>
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element?.getAttribute("contenteditable") === "true";
const handleModuleSwitch = (keyCombination: string) => {
const modules: Record<string, string> = {
@@ -84,7 +86,10 @@ const KeyPressListener: React.FC = () => {
setToggleView(!toggleTo2D);
if (toggleTo2D) {
setSelectedWallAsset(null);
setToggleUI(localStorage.getItem("navBarUiLeft") !== "false", localStorage.getItem("navBarUiRight") !== "false");
setToggleUI(
localStorage.getItem("navBarUiLeft") !== "false",
localStorage.getItem("navBarUiRight") !== "false"
);
} else {
setToggleUI(false, false);
}
@@ -188,10 +193,15 @@ const KeyPressListener: React.FC = () => {
setIsLogListVisible(false);
setIsRenameMode(false);
setDfxUploaded([]);
setSelectedComment(null);
setSelectedThread(null);
}
if (!keyCombination || ["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") return;
if (
!keyCombination ||
["F5", "F11", "F12"].includes(event.key) ||
keyCombination === "Ctrl+R"
)
return;
event.preventDefault();
@@ -243,7 +253,18 @@ const KeyPressListener: React.FC = () => {
window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeModule, toggleUIRight, toggleUILeft, toggleView, showShortcuts, isPlaying, isLogListVisible, hidePlayer, isRenameMode, selectedAssets]);
}, [
activeModule,
toggleUIRight,
toggleUILeft,
toggleView,
showShortcuts,
isPlaying,
isLogListVisible,
hidePlayer,
isRenameMode,
selectedAssets,
]);
return null;
};