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

View File

@@ -31,10 +31,10 @@ import SelectedFloorProperties from "./properties/SelectedFloorProperties";
import SelectedDecalProperties from "./properties/SelectedDecalProperties"; import SelectedDecalProperties from "./properties/SelectedDecalProperties";
import SelectedAisleProperties from "./properties/SelectedAisleProperties"; import SelectedAisleProperties from "./properties/SelectedAisleProperties";
import SelectedZoneProperties from "./properties/SelectedZoneProperties"; import SelectedZoneProperties from "./properties/SelectedZoneProperties";
import ThreadProperties from "./properties/ThreadProperties";
import ResourceManagement from "./resourceManagement/ResourceManagement"; import ResourceManagement from "./resourceManagement/ResourceManagement";
import { useSceneContext } from "../../../modules/scene/sceneContext"; import { useSceneContext } from "../../../modules/scene/sceneContext";
import ThreadDetails from "./properties/eventProperties/components/ThreadDetails";
type DisplayComponent = type DisplayComponent =
| "versionHistory" | "versionHistory"
@@ -54,13 +54,12 @@ type DisplayComponent =
| "analysis" | "analysis"
| "visualization" | "visualization"
| "resourceManagement" | "resourceManagement"
| "threadDetails" | "threadProperties"
| "none"; | "none";
const SideBarRight: React.FC = () => { const SideBarRight: React.FC = () => {
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
const { activeTool } = useActiveTool(); const { activeTool } = useActiveTool();
console.log("activeTool: ", activeTool);
const { toggleUIRight } = useToggleStore(); const { toggleUIRight } = useToggleStore();
const { toolMode } = useToolMode(); const { toolMode } = useToolMode();
const { subModule, setSubModule } = useSubModuleStore(); const { subModule, setSubModule } = useSubModuleStore();
@@ -68,12 +67,10 @@ const SideBarRight: React.FC = () => {
useBuilderStore(); useBuilderStore();
const { selectedEventData } = useSelectedEventData(); const { selectedEventData } = useSelectedEventData();
const { selectedEventSphere } = useSelectedEventSphere(); const { selectedEventSphere } = useSelectedEventSphere();
const { versionStore, assetStore, threadStore } = useSceneContext(); const { versionStore, assetStore } = useSceneContext();
const { selectedAssets } = assetStore(); const { selectedAssets } = assetStore();
const { viewVersionHistory, setVersionHistoryVisible } = versionStore(); const { viewVersionHistory, setVersionHistoryVisible } = versionStore();
const { isComparing } = useIsComparing(); const { isComparing } = useIsComparing();
const { threads } = threadStore();
console.log("threads: ", threads);
const [displayComponent, setDisplayComponent] = useState<DisplayComponent>("none"); const [displayComponent, setDisplayComponent] = useState<DisplayComponent>("none");
@@ -119,7 +116,12 @@ const SideBarRight: React.FC = () => {
return; return;
} }
} }
if (
(activeModule === "builder" || activeModule === "simulation") &&
activeTool === "comment"
) {
setDisplayComponent("threadProperties");
}
if ( if (
activeModule === "simulation" || activeModule === "simulation" ||
(activeModule === "builder" && activeTool !== "comment") (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) { if (selectedAssets.length === 1) {
setDisplayComponent("assetProperties"); setDisplayComponent("assetProperties");
return; return;
@@ -230,12 +236,8 @@ const SideBarRight: React.FC = () => {
setDisplayComponent("globalProperties"); setDisplayComponent("globalProperties");
return; return;
} }
if (activeModule === "builder" && activeTool === "comment") {
setDisplayComponent("threadDetails");
}
} }
// setDisplayComponent("none");
setDisplayComponent("none");
}, [ }, [
viewVersionHistory, viewVersionHistory,
activeModule, activeModule,
@@ -287,8 +289,8 @@ const SideBarRight: React.FC = () => {
return <Visualization />; return <Visualization />;
case "resourceManagement": case "resourceManagement":
return <ResourceManagement />; return <ResourceManagement />;
case "threadDetails": case "threadProperties":
return <ThreadDetails />; return <ThreadProperties />;
default: default:
return null; 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 React, { useMemo, useState } from "react";
import { getAvatarColor } from "../../../../modules/collaboration/functions/getAvatarColor"; import {
import { getUserData } from "../../../../functions/getUserData"; // getAvatarColor,
getAvatarColorUsingUserID,
} from "../../../../modules/collaboration/functions/getAvatarColor";
import { getRelativeTime } from "../function/getRelativeTime"; import { getRelativeTime } from "../function/getRelativeTime";
interface CommentThreadsProps { interface CommentThreadsProps {
commentClicked: () => void; commentClicked: () => void;
thread?: ThreadSchema; thread?: ThreadSchema;
selectedThread: ThreadSchema | null;
} }
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, thread }) => { const CommentThreads = ({ commentClicked, thread, selectedThread }: CommentThreadsProps) => {
const [expand, setExpand] = useState(false); const [expand, setExpand] = useState(false);
const commentsedUsers = [{ creatorId: "1" }]; const commentedUsers = useMemo(() => {
const { userName } = getUserData(); if (!thread) return [];
function getUsername(userId: string) { const usersMap = new Map<string, { creatorId: string; creatorName: string }>();
const UserName = userName?.charAt(0).toUpperCase() || "user";
return UserName; 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") { function getDetails(type?: "clicked") {
if (type === "clicked") { if (type === "clicked") {
@@ -27,36 +44,49 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, thread
} }
} }
if (!thread) return null;
return ( return (
<div className="comments-threads-wrapper"> <div className="comments-threads-wrapper">
<button <button
onPointerEnter={() => getDetails()} onPointerEnter={() => {
getDetails();
}}
onPointerLeave={() => getDetails()} onPointerLeave={() => getDetails()}
onClick={() => getDetails("clicked")} onClick={() => getDetails("clicked")}
className={`comments-threads-container ${expand ? "open" : "closed"} unread`} className={`comments-threads-container ${
expand || (selectedThread && selectedThread.threadId === thread.threadId)
? "open"
: "closed"
} unread`}
> >
<div className="users-commented"> <div className="users-commented">
{commentsedUsers.map((val, i) => ( {thread?.creatorId &&
<div commentedUsers.map((val, i) => (
className="users" <div
key={val.creatorId} className="users"
style={{ key={val.creatorId}
background: getAvatarColor(i, getUsername(val.creatorId)), style={{
}} // background: getAvatarColor(i, getUsername(val.creatorId)),
> background: getAvatarColorUsingUserID(val.creatorId),
{getUsername(val.creatorId)[0]} }}
</div> >
))} {val.creatorName.charAt(0)}
</div>
))}
</div> </div>
<div className={`last-comment-details ${expand ? "expand" : ""}`}> <div className={`last-comment-details ${expand ? "expand" : ""}`}>
<div className="header"> <div className="header">
<div className="user-name">{userName}</div> <div className="user-name">{thread.creatorName}</div>
<div className="time">{thread?.createdAt && getRelativeTime(thread.createdAt)}</div> <div className="time">
{thread?.createdAt && getRelativeTime(thread.createdAt)}
</div>
</div> </div>
<div className="message">{thread?.threadTitle}</div> <div className="message">{thread?.threadTitle}</div>
{thread && thread?.comments.length > 0 && ( {thread && thread?.comments.length > 0 && (
<div className="comments"> <div className="comments">
{thread?.comments.length} {thread?.comments.length === 1 ? "comment" : "replies"} {thread?.comments.length}{" "}
{thread?.comments.length === 1 ? "comment" : "replies"}
</div> </div>
)} )}
</div> </div>

View File

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

View File

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

View File

@@ -36,12 +36,13 @@ export function getAvatarColor(index: number, userId?: string): string {
// Find a new color not already assigned // Find a new color not already assigned
const usedColors = Object.values(userColors); 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 // Assign a new color
const assignedColor = availableColors.length > 0 const assignedColor =
? availableColors[0] availableColors.length > 0
: avatarColors[index % avatarColors.length]; ? availableColors[0]
: avatarColors[index % avatarColors.length];
userColors[userId] = assignedColor; 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 // Fallback: Assign a color using the index if no userId or local storage is unavailable
return avatarColors[index % avatarColors.length]; 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 UserResponses from "./userResponses";
import BuilderResponses from "./builderResponses"; import BuilderResponses from "./builderResponses";
import SimulationResponses from "./simulationResponses"; import SimulationResponses from "./simulationResponses";
import CollaborationResponses from "./collaborationResponses";
export default function SocketResponses() { export default function SocketResponses() {
return ( return (
@@ -10,6 +11,8 @@ export default function SocketResponses() {
<BuilderResponses /> <BuilderResponses />
<SimulationResponses /> <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 { Html, TransformControls } from "@react-three/drei";
import { Group, Object3D, Vector3 } from "three"; import { Group, Object3D, Vector3 } from "three";
import { usePlayButtonStore } from "../../../../../store/ui/usePlayButtonStore"; import { usePlayButtonStore } from "../../../../../store/ui/usePlayButtonStore";
import { useSelectedComment } from "../../../../../store/builder/store";
import CommentThreads from "../../../../../components/ui/collaboration/threads/CommentThreads"; import CommentThreads from "../../../../../components/ui/collaboration/threads/CommentThreads";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { useSceneContext } from "../../../../scene/sceneContext";
function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) { function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
const { threadStore } = useSceneContext();
const { selectedThread, setSelectedThread, setPosition2Dstate } = threadStore();
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 [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();
// useEffect(() => { useEffect(() => {
// const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
// const keyCombination = detectModifierKeys(e); const keyCombination = detectModifierKeys(e);
// if (!selectedComment) return; if (!selectedThread) return;
// if (keyCombination === "G") { if (keyCombination === "G") {
// setTransformMode((prev) => (prev === "translate" ? null : "translate")); setTransformMode((prev) => (prev === "translate" ? null : "translate"));
// } }
// if (keyCombination === "R") { if (keyCombination === "R") {
// setTransformMode((prev) => (prev === "rotate" ? null : "rotate")); setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
// } }
// }; };
// window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
// return () => window.removeEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
// }, [selectedComment]); }, [selectedThread]);
const commentClicked = () => { const commentClicked = () => {
setSelectedComment(thread); setSelectedThread(thread);
const position = new Vector3(thread.position[0], thread.position[1], thread.position[2]); const position = new Vector3(thread.position[0], thread.position[1], thread.position[2]);
position.project(camera); position.project(camera);
@@ -48,10 +50,10 @@ function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
}; };
useEffect(() => { useEffect(() => {
if (!selectedComment || selectedComment.threadId !== thread.threadId) { if (!selectedThread || selectedThread.threadId !== thread.threadId) {
setSelectedObject(null); setSelectedObject(null);
} }
}, [selectedComment]); }, [selectedThread]);
if (thread.state === "inactive" || isPlaying) return null; if (thread.state === "inactive" || isPlaying) return null;
@@ -68,10 +70,14 @@ function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
// rotation={comment.rotation} // rotation={comment.rotation}
className="comments-main-wrapper" className="comments-main-wrapper"
> >
<CommentThreads commentClicked={commentClicked} thread={thread} /> <CommentThreads
commentClicked={commentClicked}
thread={thread}
selectedThread={selectedThread}
/>
</Html> </Html>
</group> </group>
{/* {selectedObject && transformMode && ( {selectedObject && transformMode && (
<TransformControls <TransformControls
object={selectedObject} object={selectedObject}
mode={transformMode} mode={transformMode}
@@ -79,7 +85,7 @@ function ThreadInstance({ thread }: { readonly thread: ThreadSchema }) {
console.log("sad"); console.log("sad");
}} }}
/> />
)} */} )}
</> </>
); );
} }

View File

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

View File

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

View File

@@ -1,8 +1,14 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; 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 { 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", method: "POST",
headers: { headers: {
Authorization: "Bearer <access_token>", Authorization: "Bearer <access_token>",
@@ -10,8 +16,16 @@ export const addCommentsApi = async (projectId: any, comment: string, threadId:
token: localStorage.getItem("token") || "", token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "", 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"); const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) { if (newAccessToken) {
@@ -23,6 +37,7 @@ export const addCommentsApi = async (projectId: any, comment: string, threadId:
} }
const result = await response.json(); const result = await response.json();
console.log("result: ", result);
return result; return result;
} catch { } catch {

View File

@@ -17,7 +17,14 @@ export const createThreadApi = async (
token: localStorage.getItem("token") || "", token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "", 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"); const newAccessToken = response.headers.get("x-access-token");

View File

@@ -31,6 +31,7 @@ export const deleteThreadApi = async (
} }
const result = await response.json(); const result = await response.json();
console.log('result: ', result);
return result; return result;
} catch { } catch {
echo.error("Failed to delete thread"); 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}`; 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 { 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", method: "POST",
headers: { headers: {
Authorization: "Bearer <access_token>", Authorization: "Bearer <access_token>",
@@ -23,6 +29,7 @@ export const editCommentsApi = async (projectId: any, comment: string, commentId
} }
const result = await response.json(); const result = await response.json();
console.log("result: ", result);
return result; return result;
} catch { } catch {

View File

@@ -435,15 +435,6 @@ export const useCompareProductDataStore = create<{
setCompareProductsData: (x) => set({ compareProductsData: x }), setCompareProductsData: (x) => set({ compareProductsData: x }),
})); }));
export const useSelectedComment = create<any>((set: any) => ({
selectedComment: null,
setSelectedComment: (x: any) => set({ selectedComment: x }),
position2Dstate: {},
setPosition2Dstate: (x: any) => set({ position2Dstate: x }),
commentPositionState: null,
setCommentPositionState: (x: any) => set({ commentPositionState: x }),
}));
export const useSelectedPath = create<any>((set: any) => ({ export const useSelectedPath = create<any>((set: any) => ({
selectedPath: "auto", selectedPath: "auto",
setSelectedPath: (x: any) => set({ selectedPath: x }), setSelectedPath: (x: any) => set({ selectedPath: x }),

View File

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

View File

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

View File

@@ -1503,12 +1503,24 @@
&.yellow-black { &.yellow-black {
background-color: black; background-color: black;
background-size: 10px 10px; 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 { &.white-black {
background-color: black; background-color: black;
background-size: 10px 10px; 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 { .toggle-sidebar-ui-button {
svg { svg {
@@ -2099,7 +2204,11 @@
&:nth-child(2) { &:nth-child(2) {
&::after { &::after {
// @include gradient-by-child(4); // Second child uses the second color // @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%; width: 100%;
height: 100%; height: 100%;
font-size: var(--font-size-regular); 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; pointer-events: none;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
opacity: 0; opacity: 0;

View File

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

View File

@@ -7,7 +7,6 @@ import {
useDfxUpload, useDfxUpload,
useRenameModeStore, useRenameModeStore,
useIsComparing, useIsComparing,
useSelectedComment,
useShortcutStore, useShortcutStore,
useToggleView, useToggleView,
useToolMode, useToolMode,
@@ -24,7 +23,7 @@ import { useSceneContext } from "../../modules/scene/sceneContext";
const KeyPressListener: React.FC = () => { const KeyPressListener: React.FC = () => {
const { comparisonScene, clearComparisonState } = useSimulationState(); const { comparisonScene, clearComparisonState } = useSimulationState();
const { activeModule, setActiveModule } = useModuleStore(); const { activeModule, setActiveModule } = useModuleStore();
const { assetStore, versionStore } = useSceneContext(); const { assetStore, versionStore, threadStore } = useSceneContext();
const { selectedAssets } = assetStore(); const { selectedAssets } = assetStore();
const { setSubModule } = useSubModuleStore(); const { setSubModule } = useSubModuleStore();
const { setActiveSubTool } = useActiveSubTool(); const { setActiveSubTool } = useActiveSubTool();
@@ -43,9 +42,12 @@ const KeyPressListener: React.FC = () => {
const { isRenameMode, setIsRenameMode } = useRenameModeStore(); const { isRenameMode, setIsRenameMode } = useRenameModeStore();
const { setSelectedWallAsset } = useBuilderStore(); const { setSelectedWallAsset } = useBuilderStore();
const { setCreateNewVersion, setVersionHistoryVisible } = versionStore(); const { setCreateNewVersion, setVersionHistoryVisible } = versionStore();
const { setSelectedComment } = useSelectedComment(); const { setSelectedThread } = threadStore();
const { setDfxUploaded } = useDfxUpload(); 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 handleModuleSwitch = (keyCombination: string) => {
const modules: Record<string, string> = { const modules: Record<string, string> = {
@@ -84,7 +86,10 @@ const KeyPressListener: React.FC = () => {
setToggleView(!toggleTo2D); setToggleView(!toggleTo2D);
if (toggleTo2D) { if (toggleTo2D) {
setSelectedWallAsset(null); setSelectedWallAsset(null);
setToggleUI(localStorage.getItem("navBarUiLeft") !== "false", localStorage.getItem("navBarUiRight") !== "false"); setToggleUI(
localStorage.getItem("navBarUiLeft") !== "false",
localStorage.getItem("navBarUiRight") !== "false"
);
} else { } else {
setToggleUI(false, false); setToggleUI(false, false);
} }
@@ -188,10 +193,15 @@ const KeyPressListener: React.FC = () => {
setIsLogListVisible(false); setIsLogListVisible(false);
setIsRenameMode(false); setIsRenameMode(false);
setDfxUploaded([]); 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(); event.preventDefault();
@@ -243,7 +253,18 @@ const KeyPressListener: React.FC = () => {
window.addEventListener("keydown", handleKeyPress); window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress); return () => window.removeEventListener("keydown", handleKeyPress);
// eslint-disable-next-line react-hooks/exhaustive-deps // 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; return null;
}; };