refactoring thread functionalities including socketresponses
This commit is contained in:
@@ -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 />}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const ThreadDetails = () => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default ThreadDetails;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
122
app/src/modules/collaboration/socket/collaborationResponses.tsx
Normal file
122
app/src/modules/collaboration/socket/collaborationResponses.tsx
Normal 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;
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
app/src/types/collaborationTypes.d.ts
vendored
4
app/src/types/collaborationTypes.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user