Merge remote-tracking branch 'origin/feature/threaded-comments' into v3
This commit is contained in:
commit
78e57ab9fa
|
@ -1,13 +1,21 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
||||||
|
import { getUserData } from "../../../functions/getUserData";
|
||||||
|
import { getAllThreads } from "../../../services/factoryBuilder/comments/getAllThreads";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
|
||||||
|
import { getRelativeTime } from "./function/getRelativeTime";
|
||||||
|
import { useSelectedComment } from "../../../store/builder/store";
|
||||||
|
|
||||||
interface CommentThreadsProps {
|
interface CommentThreadsProps {
|
||||||
commentClicked: () => void;
|
commentClicked: () => void;
|
||||||
|
comment?: CommentSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
|
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked, comment }) => {
|
||||||
const [expand, setExpand] = useState(false);
|
const [expand, setExpand] = useState(false);
|
||||||
const commentsedUsers = [{ creatorId: "1" }];
|
const commentsedUsers = [{ creatorId: "1" }];
|
||||||
|
const { userName } = getUserData();
|
||||||
|
|
||||||
const CommentDetails = {
|
const CommentDetails = {
|
||||||
state: "active",
|
state: "active",
|
||||||
|
@ -16,26 +24,26 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
|
||||||
createdAt: "2 hours ago",
|
createdAt: "2 hours ago",
|
||||||
comment: "Thread check",
|
comment: "Thread check",
|
||||||
lastUpdatedAt: "string",
|
lastUpdatedAt: "string",
|
||||||
replies: [
|
comments: [
|
||||||
{
|
{
|
||||||
replyId: "string",
|
replyId: "string",
|
||||||
creatorId: "string",
|
creatorId: "string",
|
||||||
createdAt: "string",
|
createdAt: "string",
|
||||||
lastUpdatedAt: "string",
|
lastUpdatedAt: "string",
|
||||||
reply: "string",
|
comment: "string",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replyId: "string",
|
replyId: "string",
|
||||||
creatorId: "string",
|
creatorId: "string",
|
||||||
createdAt: "string",
|
createdAt: "string",
|
||||||
lastUpdatedAt: "string",
|
lastUpdatedAt: "string",
|
||||||
reply: "string",
|
comment: "string",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
function getUsername(userId: string) {
|
function getUsername(userId: string) {
|
||||||
const UserName = "username";
|
const UserName = userName?.charAt(0).toUpperCase() || "user";
|
||||||
return UserName;
|
return UserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,14 +56,14 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="comments-threads-wrapper">
|
<div className="comments-threads-wrapper">
|
||||||
<button
|
<button
|
||||||
onPointerEnter={() => getDetails()}
|
onPointerEnter={() => getDetails()}
|
||||||
onPointerLeave={() => getDetails()}
|
onPointerLeave={() => getDetails()}
|
||||||
onClick={() => getDetails("clicked")}
|
onClick={() => getDetails("clicked")}
|
||||||
className={`comments-threads-container ${
|
className={`comments-threads-container ${expand ? "open" : "closed"
|
||||||
expand ? "open" : "closed"
|
|
||||||
} unread`}
|
} unread`}
|
||||||
>
|
>
|
||||||
<div className="users-commented">
|
<div className="users-commented">
|
||||||
|
@ -70,24 +78,37 @@ const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
|
||||||
{getUsername(val.creatorId)[0]}
|
{getUsername(val.creatorId)[0]}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{/* {commentsedUsers.map((val, i) => (
|
||||||
|
<div
|
||||||
|
className="users"
|
||||||
|
key={val.creatorId}
|
||||||
|
style={{
|
||||||
|
background: getAvatarColor(i, getUsername(val.creatorId)),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getUsername(val.creatorId)[0]}
|
||||||
|
</div>
|
||||||
|
))} */}
|
||||||
</div>
|
</div>
|
||||||
<div className={`last-comment-details ${expand ? "expand" : ""}`}>
|
<div className={`last-comment-details ${expand ? "expand" : ""}`}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="user-name">
|
<div className="user-name">
|
||||||
{getUsername(CommentDetails.creatorId)}
|
{userName}
|
||||||
|
{/* {getUsername(CommentDetails.creatorId)} */}
|
||||||
</div>
|
</div>
|
||||||
<div className="time">{CommentDetails.createdAt}</div>
|
<div className="time">{comment?.createdAt && getRelativeTime(comment.createdAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="message">{CommentDetails.comment}</div>
|
<div className="message">{comment?.threadTitle}</div>
|
||||||
{CommentDetails.replies.length > 0 && (
|
{comment && comment?.comments.length > 0 && (
|
||||||
<div className="replies">
|
<div className="comments">
|
||||||
{CommentDetails.replies.length}{" "}
|
{comment && comment?.comments.length}{" "}
|
||||||
{CommentDetails.replies.length === 1 ? "reply" : "replies"}
|
{comment && comment?.comments.length === 1 ? "comment" : "replies"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,44 +2,181 @@ import React, { useEffect, useRef, useState } from "react";
|
||||||
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
||||||
import { KebabIcon } from "../../icons/ExportCommonIcons";
|
import { KebabIcon } from "../../icons/ExportCommonIcons";
|
||||||
import { adjustHeight } from "./function/textAreaHeightAdjust";
|
import { adjustHeight } from "./function/textAreaHeightAdjust";
|
||||||
|
import { getUserData } from "../../../functions/getUserData";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { deleteCommentApi } from "../../../services/factoryBuilder/comments/deleteCommentApi";
|
||||||
|
import { addCommentsApi } from "../../../services/factoryBuilder/comments/addCommentsApi";
|
||||||
|
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
|
||||||
|
import { useSelectedComment, useSocketStore } from "../../../store/builder/store";
|
||||||
|
import { getRelativeTime } from "./function/getRelativeTime";
|
||||||
|
import { editThreadTitleApi } from "../../../services/factoryBuilder/comments/editThreadTitleApi";
|
||||||
|
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
val: Reply | CommentSchema;
|
val: Reply | CommentSchema;
|
||||||
|
// val: Reply | CommentSchema;
|
||||||
i: number;
|
i: number;
|
||||||
|
setMessages?: React.Dispatch<React.SetStateAction<Reply[]>>
|
||||||
|
setIsEditable?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setEditedThread?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setMode?: React.Dispatch<React.SetStateAction<'create' | 'edit' | null>>
|
||||||
|
isEditable?: boolean;
|
||||||
|
isEditableThread?: boolean
|
||||||
|
editedThread?: boolean;
|
||||||
|
mode?: 'create' | 'edit' | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEditable, setEditedThread, editedThread, isEditableThread, setMode }) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
|
const { comments, updateComment, updateReply, removeReply } = useCommentStore();
|
||||||
const [openOptions, setOpenOptions] = useState(false);
|
const [openOptions, setOpenOptions] = useState(false);
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const { threadSocket } = useSocketStore();
|
||||||
|
const { userName, userId, organization } = getUserData();
|
||||||
|
const [isEditComment, setIsEditComment] = useState(false)
|
||||||
|
|
||||||
|
const { selectedComment, setCommentPositionState } = useSelectedComment();
|
||||||
|
|
||||||
// input
|
// input
|
||||||
const [value, setValue] = useState<string>(
|
const [value, setValue] = useState<string>(
|
||||||
"reply" in val ? val.reply : val.comment
|
"comment" in val ? val.comment : val.threadTitle
|
||||||
);
|
);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const currentUser = "1";
|
const currentUser = "1";
|
||||||
|
|
||||||
const UserName = "username";
|
// const UserName = "username";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef.current) adjustHeight(textareaRef.current);
|
if (textareaRef.current) adjustHeight(textareaRef.current);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
function handleCancelAction() {
|
function handleCancelAction() {
|
||||||
setIsEditing(false);
|
setCommentPositionState(null)
|
||||||
|
setIsEditable && setIsEditable(true);
|
||||||
|
setIsEditComment(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSaveAction() {
|
const handleSaveAction = async () => {
|
||||||
setIsEditing(false);
|
|
||||||
|
if (!projectId) return
|
||||||
|
|
||||||
|
if (isEditableThread && editedThread) {
|
||||||
|
try {
|
||||||
|
// const editThreadTitle = await editThreadTitleApi(projectId, (val as CommentSchema).threadId, value)
|
||||||
|
// if (editThreadTitle.message == "ThreadTitle updated Successfully") {
|
||||||
|
// const editedThread: CommentSchema = {
|
||||||
|
// state: 'active',
|
||||||
|
// threadId: editThreadTitle.data.replyId,
|
||||||
|
// creatorId: userId,
|
||||||
|
// createdAt: getRelativeTime(editThreadTitle.data.createdAt),
|
||||||
|
// threadTitle: value,
|
||||||
|
// lastUpdatedAt: new Date().toISOString(),
|
||||||
|
// position: editThreadTitle.data.position,
|
||||||
|
// rotation: [0, 0, 0],
|
||||||
|
// comments: []
|
||||||
|
// }
|
||||||
|
// updateComment((val as CommentSchema).threadId, editedThread)
|
||||||
|
// }
|
||||||
|
// projectId, userId, threadTitle, organization, threadId
|
||||||
|
const threadEdit = {
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
threadTitle: value,
|
||||||
|
organization,
|
||||||
|
threadId: (val as CommentSchema).threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteAction() {
|
threadSocket.emit('v1:thread:updateTitle', threadEdit)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (mode === "edit") {
|
||||||
|
try {
|
||||||
|
// const editComments = await addCommentsApi(projectId, value, selectedComment?.threadId, (val as Reply).replyId)
|
||||||
|
//
|
||||||
|
// const commentData = {
|
||||||
|
// replyId: `${editComments.data?.replyId}`,
|
||||||
|
// creatorId: `${userId}`,
|
||||||
|
// createdAt: getRelativeTime(editComments.data?.createdAt),
|
||||||
|
// lastUpdatedAt: "2 hrs ago",
|
||||||
|
// comment: value,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// updateReply((val as CommentSchema).threadId, (val as Reply)?.replyId, commentData);
|
||||||
|
|
||||||
|
if (threadSocket) {
|
||||||
|
// projectId, userId, comment, organization, threadId
|
||||||
|
const editComment = {
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
comment: value,
|
||||||
|
organization,
|
||||||
|
threadId: selectedComment?.threadId,
|
||||||
|
commentId: (val as Reply).replyId ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
threadSocket.emit("v1-Comment:add", editComment);
|
||||||
|
setIsEditable && setIsEditable(true);
|
||||||
|
setEditedThread && setEditedThread(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// setValue("");
|
||||||
|
setIsEditComment(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteAction = async (replyID: any) => {
|
||||||
|
if (!projectId) return
|
||||||
setOpenOptions(false);
|
setOpenOptions(false);
|
||||||
|
try {
|
||||||
|
// const deletedComment = await deleteCommentApi(projectId, selectedComment?.threadId, (val as Reply).replyId)
|
||||||
|
//
|
||||||
|
// if (deletedComment === "'Thread comment deleted Successfully'") {
|
||||||
|
// setMessages && setMessages(prev => prev.filter(message => message.replyId !== replyID));
|
||||||
|
// removeReply(val.creatorId, replyID)
|
||||||
|
// }
|
||||||
|
if (threadSocket && setMessages) {
|
||||||
|
|
||||||
|
|
||||||
|
// projectId, userId, commentId, organization, threadId
|
||||||
|
const deleteComment = {
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
commentId: (val as Reply).replyId,
|
||||||
|
organization,
|
||||||
|
threadId: selectedComment?.threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setMessages(prev => prev.filter(message => message.replyId !== (val as Reply).replyId))
|
||||||
|
|
||||||
|
removeReply(selectedComment?.threadId, (val as Reply).replyId); // Remove listener after response
|
||||||
|
threadSocket.emit("v1-Comment:delete", deleteComment);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
const length = textareaRef.current.value.length;
|
||||||
|
textareaRef.current.setSelectionRange(length, length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isEditing ? (
|
{isEditComment ? (
|
||||||
<div className="edit-container">
|
<div className="edit-container">
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -49,6 +186,7 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
style={{ resize: "none" }}
|
style={{ resize: "none" }}
|
||||||
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="actions-container">
|
<div className="actions-container">
|
||||||
|
@ -77,16 +215,16 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
||||||
<div className="message-container">
|
<div className="message-container">
|
||||||
<div
|
<div
|
||||||
className="profile"
|
className="profile"
|
||||||
style={{ background: getAvatarColor(i, UserName) }}
|
style={{ background: getAvatarColor(i, userName) }}
|
||||||
>
|
>
|
||||||
{UserName[0]}
|
{userName?.charAt(0).toUpperCase() || "user"}
|
||||||
</div>
|
</div>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="user-details">
|
<div className="user-details">
|
||||||
<div className="user-name">{UserName}</div>
|
<div className="user-name">{userName}</div>
|
||||||
<div className="time">{val.createdAt}</div>
|
<div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div>
|
||||||
</div>
|
</div>
|
||||||
{val.creatorId === currentUser && (
|
{(val as Reply).creatorId === userId && (
|
||||||
<div className="more-options">
|
<div className="more-options">
|
||||||
<button
|
<button
|
||||||
className="more-options-button"
|
className="more-options-button"
|
||||||
|
@ -100,27 +238,30 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
||||||
<div className="options-list">
|
<div className="options-list">
|
||||||
<button
|
<button
|
||||||
className="option"
|
className="option"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setMode && setMode("edit")
|
||||||
setOpenOptions(false);
|
setOpenOptions(false);
|
||||||
setIsEditing(true);
|
setEditedThread && setEditedThread(true);
|
||||||
|
setIsEditComment(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
{!(isEditableThread) && <button
|
||||||
className="option"
|
className="option"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteAction();
|
handleDeleteAction((val as Reply).replyId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="message">
|
<div className="message">
|
||||||
{"reply" in val ? val.reply : val.comment}
|
{"comment" in val ? val.comment : val.threadTitle}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
|
|
|
@ -3,36 +3,67 @@ import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
|
||||||
import Messages from "./Messages";
|
import Messages from "./Messages";
|
||||||
import { ExpandIcon } from "../../icons/SimulationIcons";
|
import { ExpandIcon } from "../../icons/SimulationIcons";
|
||||||
import { adjustHeight } from "./function/textAreaHeightAdjust";
|
import { adjustHeight } from "./function/textAreaHeightAdjust";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useSelectedComment, useSocketStore } from "../../../store/builder/store";
|
||||||
|
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
|
||||||
|
import { getUserData } from "../../../functions/getUserData";
|
||||||
|
import ThreadSocketResponsesDev from "../../../modules/collaboration/socket/threadSocketResponses.dev";
|
||||||
|
import { addCommentsApi } from "../../../services/factoryBuilder/comments/addCommentsApi";
|
||||||
|
import { deleteThreadApi } from "../../../services/factoryBuilder/comments/deleteThreadApi";
|
||||||
|
import { createThreadApi } from "../../../services/factoryBuilder/comments/createThreadApi";
|
||||||
|
import { getRelativeTime } from "./function/getRelativeTime";
|
||||||
|
|
||||||
|
|
||||||
const ThreadChat: React.FC = () => {
|
const ThreadChat: React.FC = () => {
|
||||||
|
const { userId, organization } = getUserData();
|
||||||
const [openThreadOptions, setOpenThreadOptions] = useState(false);
|
const [openThreadOptions, setOpenThreadOptions] = useState(false);
|
||||||
const [inputActive, setInputActive] = useState(false);
|
const [inputActive, setInputActive] = useState(false);
|
||||||
const [value, setValue] = useState<string>("");
|
const [value, setValue] = useState<string>("");
|
||||||
|
const { addComment, removeReply, removeComment, addReply, comments } = useCommentStore();
|
||||||
|
const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState, position2Dstate } = useSelectedComment()
|
||||||
|
const [mode, setMode] = useState<'create' | 'edit' | null>('create');
|
||||||
|
const [isEditable, setIsEditable] = useState(false);
|
||||||
|
const [editedThread, setEditedThread] = useState(false);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [messages, setMessages] = useState<Reply[]>([])
|
||||||
|
const { projectId } = useParams();
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [selectedDiv, setSelectedDiv] = useState(true);
|
||||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||||
const [position, setPosition] = useState({ x: 100, y: 100 });
|
const [position, setPosition] = useState({ x: position2Dstate.x, y: position2Dstate.y });
|
||||||
|
const { threadSocket } = useSocketStore();
|
||||||
|
const modeRef = useRef<'create' | 'edit' | null>(null);
|
||||||
|
const messagesRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const messages = [
|
useEffect(() => {
|
||||||
{
|
modeRef.current = mode;
|
||||||
replyId: "user 1",
|
}, [mode]);
|
||||||
creatorId: "1",
|
|
||||||
createdAt: "2 hrs ago",
|
useEffect(() => {
|
||||||
lastUpdatedAt: "2 hrs ago",
|
if (comments.length > 0 && selectedComment) {
|
||||||
reply:
|
|
||||||
"reply testing reply content 1, reply testing reply content 1reply testing reply content 1",
|
|
||||||
},
|
const allMessages = comments
|
||||||
{
|
.flatMap((val: any) =>
|
||||||
replyId: "user 2",
|
val?.threadId === selectedComment?.threadId ? val.comments : []
|
||||||
creatorId: "2",
|
)
|
||||||
createdAt: "2 hrs ago",
|
.map((c) => {
|
||||||
lastUpdatedAt: "2 hrs ago",
|
return {
|
||||||
reply: "reply 2",
|
replyId: c._id ?? "",
|
||||||
},
|
creatorId: c.creatorId || c.userId,
|
||||||
];
|
createdAt: c.createdAt,
|
||||||
|
lastUpdatedAt: "1 hr ago",
|
||||||
|
comment: c.comment,
|
||||||
|
_id: c._id ?? "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setMessages(allMessages);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [selectedComment])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef.current) adjustHeight(textareaRef.current);
|
if (textareaRef.current) adjustHeight(textareaRef.current);
|
||||||
|
@ -44,6 +75,19 @@ const ThreadChat: React.FC = () => {
|
||||||
|
|
||||||
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
|
// Avoid dragging if a button, icon, textarea etc. was clicked
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (
|
||||||
|
target.closest("button") ||
|
||||||
|
target.closest(".sent-button") ||
|
||||||
|
target.closest("textarea") ||
|
||||||
|
target.closest(".options-button") ||
|
||||||
|
target.closest(".options-list") ||
|
||||||
|
target.closest(".send-message-wrapper") ||
|
||||||
|
target.closest(".options delete")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wrapper = wrapperRef.current;
|
const wrapper = wrapperRef.current;
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
@ -58,18 +102,20 @@ const ThreadChat: React.FC = () => {
|
||||||
wrapper.setPointerCapture(event.pointerId);
|
wrapper.setPointerCapture(event.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
|
const updatePosition = (
|
||||||
if (!dragging) return;
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
|
allowMove: boolean = true
|
||||||
|
) => {
|
||||||
|
if (!allowMove || !wrapperRef.current) return;
|
||||||
|
|
||||||
const container = document.getElementById("work-space-three-d-canvas");
|
const container = document.getElementById("work-space-three-d-canvas");
|
||||||
const wrapper = wrapperRef.current;
|
const wrapper = wrapperRef.current;
|
||||||
if (!container || !wrapper) return;
|
if (!container || !wrapper) return;
|
||||||
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const wrapperRect = wrapper.getBoundingClientRect();
|
|
||||||
|
|
||||||
let newX = event.clientX - containerRect.left - dragOffset.x;
|
let newX = clientX - containerRect.left - dragOffset.x;
|
||||||
let newY = event.clientY - containerRect.top - dragOffset.y;
|
let newY = clientY - containerRect.top - dragOffset.y;
|
||||||
|
|
||||||
const maxX = containerRect.width - wrapper.offsetWidth;
|
const maxX = containerRect.width - wrapper.offsetWidth;
|
||||||
const maxY = containerRect.height - wrapper.offsetHeight;
|
const maxY = containerRect.height - wrapper.offsetHeight;
|
||||||
|
@ -80,6 +126,15 @@ const ThreadChat: React.FC = () => {
|
||||||
setPosition({ x: newX, y: newY });
|
setPosition({ x: newX, y: newY });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePointerMove = (e: { clientX: number; clientY: number }) => {
|
||||||
|
if (dragging) updatePosition(e, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updatePosition({ clientX: position.x, clientY: position.y }, true);
|
||||||
|
}, [selectedComment]);
|
||||||
|
|
||||||
|
|
||||||
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
|
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
|
@ -87,12 +142,146 @@ const ThreadChat: React.FC = () => {
|
||||||
if (wrapper) wrapper.releasePointerCapture(event.pointerId);
|
if (wrapper) wrapper.releasePointerCapture(event.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateComments = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
// const createComments = await addCommentsApi(projectId, value, selectedComment?.threadId)/
|
||||||
|
// if (createComments.message === 'Thread comments add Successfully' && createComments.data) {
|
||||||
|
// const commentData = {
|
||||||
|
// replyId: `${createComments.data?._id}`,
|
||||||
|
// creatorId: `${selectedComment?.threadId}`,
|
||||||
|
// createdAt: "2 hrs ago",
|
||||||
|
// lastUpdatedAt: "2 hrs ago",
|
||||||
|
// comment: value,
|
||||||
|
// }
|
||||||
|
// setMessages((prevMessages) => [
|
||||||
|
// ...prevMessages,
|
||||||
|
// commentData,
|
||||||
|
// ]);
|
||||||
|
// addReply(selectedComment?.threadId, commentData)
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (threadSocket && mode === "create") {
|
||||||
|
const addComment = {
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
comment: value,
|
||||||
|
organization,
|
||||||
|
threadId: selectedComment?.threadId
|
||||||
|
}
|
||||||
|
|
||||||
|
threadSocket.emit("v1-Comment:add", addComment);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
setInputActive(false)
|
||||||
|
}
|
||||||
|
const handleDeleteThread = async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
// const deleteThread = await deleteThreadApi(projectId, selectedComment?.threadId)
|
||||||
|
//
|
||||||
|
// if (deleteThread.message === "Thread deleted Successfully") {
|
||||||
|
// removeComment(selectedComment?.threadId)
|
||||||
|
// setSelectedComment([])
|
||||||
|
// }
|
||||||
|
if (threadSocket) {
|
||||||
|
// projectId, userId, organization, threadId
|
||||||
|
const deleteThread = {
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
organization,
|
||||||
|
threadId: selectedComment?.threadId
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedComment(null)
|
||||||
|
removeComment(selectedComment?.threadId)
|
||||||
|
threadSocket.emit("v1:thread:delete", deleteThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateThread = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!projectId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// try {
|
||||||
|
// const thread = await createThreadApi(
|
||||||
|
// projectId,
|
||||||
|
// "active",
|
||||||
|
// commentPositionState[0].position,
|
||||||
|
// [0, 0, 0],
|
||||||
|
// value
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
// if (thread.message === "Thread created Successfully" && thread?.threadData) {
|
||||||
|
//
|
||||||
|
// const comment: CommentSchema = {
|
||||||
|
// state: 'active',
|
||||||
|
// threadId: thread?.threadData?._id,
|
||||||
|
// creatorId: userId,
|
||||||
|
// createdAt: getRelativeTime(thread.threadData?.createdAt),
|
||||||
|
// threadTitle: value,
|
||||||
|
// lastUpdatedAt: new Date().toISOString(),
|
||||||
|
// position: commentPositionState[0].position,
|
||||||
|
// rotation: [0, 0, 0],
|
||||||
|
// comments: []
|
||||||
|
// }
|
||||||
|
// addComment(comment);
|
||||||
|
// setCommentPositionState(null);
|
||||||
|
// setInputActive(false);
|
||||||
|
// setSelectedComment([])
|
||||||
|
// }
|
||||||
|
|
||||||
|
const createThread = {
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
organization,
|
||||||
|
state: "active",
|
||||||
|
position: commentPositionState.position,
|
||||||
|
rotation: [0, 0, 0],
|
||||||
|
threadTitle: value
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (threadSocket) {
|
||||||
|
|
||||||
|
setInputActive(false);
|
||||||
|
threadSocket.emit("v1:thread:create", createThread);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const messagesWrapper = document.querySelector(".messages-wrapper");
|
||||||
|
if (messagesWrapper) {
|
||||||
|
messagesWrapper.scrollTop = messagesWrapper.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0)
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
className="thread-chat-wrapper"
|
className="thread-chat-wrapper"
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={(e) => handlePointerMove({ clientX: e.clientX, clientY: e.clientY })}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
@ -107,48 +296,93 @@ const ThreadChat: React.FC = () => {
|
||||||
<div className="header-wrapper">
|
<div className="header-wrapper">
|
||||||
<div className="header">Comment</div>
|
<div className="header">Comment</div>
|
||||||
<div className="header-options">
|
<div className="header-options">
|
||||||
<button
|
<div
|
||||||
className="options-button"
|
className="options-button"
|
||||||
onClick={() => setOpenThreadOptions(!openThreadOptions)}
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpenThreadOptions((prev) => !prev);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<KebabIcon />
|
<KebabIcon />
|
||||||
</button>
|
</div>
|
||||||
{openThreadOptions && (
|
{openThreadOptions && (
|
||||||
<div className="options-list">
|
<div className="options-list">
|
||||||
<div className="options">Mark as Unread</div>
|
<div className="options">Mark as Unread</div>
|
||||||
<div className="options">Mark as Resolved</div>
|
<div className="options">Mark as Resolved</div>
|
||||||
<div className="options delete">Delete Thread</div>
|
<div className="options delete" onClick={handleDeleteThread}>Delete Thread</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button className="close-button">
|
<button className="close-button" onClick={() => {
|
||||||
|
setSelectedComment(null)
|
||||||
|
setCommentPositionState(null)
|
||||||
|
}}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="messages-wrapper">
|
<div className="messages-wrapper" ref={messagesRef}>
|
||||||
{messages.map((val, i) => (
|
{selectedComment &&
|
||||||
<Messages val={val as any} i={i} key={val.replyId} />
|
<Messages val={selectedComment} i={1} key={selectedComment.creatorId} isEditableThread={true} setEditedThread={setEditedThread} editedThread={editedThread} />
|
||||||
|
}
|
||||||
|
{messages && messages.map((val, i) => (
|
||||||
|
<Messages val={val as any} i={i} key={val.replyId} setMessages={setMessages} setIsEditable={setIsEditable} isEditable={isEditable} isEditableThread={false} setMode={setMode} mode={mode} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="send-message-wrapper">
|
<div className="send-message-wrapper">
|
||||||
<div className={`input-container ${inputActive ? "active" : ""}`}>
|
<div className={`input-container ${inputActive ? "active" : ""}`}>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="type something"
|
placeholder={commentPositionState && selectedComment === null ? "Type Thread Title" : "type something"}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
onFocus={() => setInputActive(true)}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (commentPositionState && selectedComment === null) {
|
||||||
|
handleCreateThread(e);
|
||||||
|
} else {
|
||||||
|
setMode("create");
|
||||||
|
handleCreateComments(e);
|
||||||
|
textareaRef.current?.blur();
|
||||||
|
}
|
||||||
|
setValue('')
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
textareaRef.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!commentPositionState && selectedComment !== null) {
|
||||||
|
setMode("create");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus={selectedComment === null}
|
||||||
onBlur={() => setInputActive(false)}
|
onBlur={() => setInputActive(false)}
|
||||||
|
onFocus={() => setInputActive(true)}
|
||||||
style={{ resize: "none" }}
|
style={{ resize: "none" }}
|
||||||
/>
|
/>
|
||||||
<div className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}>
|
<div
|
||||||
|
className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (commentPositionState && selectedComment === null) {
|
||||||
|
handleCreateThread(e);
|
||||||
|
} else {
|
||||||
|
setMode("create");
|
||||||
|
handleCreateComments(e);
|
||||||
|
}
|
||||||
|
setValue('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ExpandIcon />
|
<ExpandIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<ThreadSocketResponsesDev setMessages={setMessages} modeRef={modeRef} messages={messages} />
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
type RelativeTimeFormatUnit = any;
|
||||||
|
export function getRelativeTime(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
|
|
||||||
|
const intervals: Record<RelativeTimeFormatUnit, number> = {
|
||||||
|
year: 31536000,
|
||||||
|
month: 2592000,
|
||||||
|
week: 604800,
|
||||||
|
day: 86400,
|
||||||
|
hour: 3600,
|
||||||
|
minute: 60,
|
||||||
|
second: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||||
|
|
||||||
|
for (const key in intervals) {
|
||||||
|
const unit = key as RelativeTimeFormatUnit;
|
||||||
|
const diff = Math.floor(diffInSeconds / intervals[unit]);
|
||||||
|
if (diff >= 1) {
|
||||||
|
return rtf.format(-diff, unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "just now";
|
||||||
|
}
|
|
@ -1,18 +1,17 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useActiveTool } from "../../../store/builder/store"
|
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import { MathUtils, Vector3 } from "three";
|
import { MathUtils, Vector3 } from "three";
|
||||||
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
|
|
||||||
import CommentInstances from "./instances/commentInstances";
|
import CommentInstances from "./instances/commentInstances";
|
||||||
import { Sphere } from "@react-three/drei";
|
import { Sphere } from "@react-three/drei";
|
||||||
|
import { useActiveTool, useSelectedComment } from "../../../store/builder/store";
|
||||||
|
|
||||||
|
|
||||||
function CommentsGroup() {
|
function CommentsGroup() {
|
||||||
const { gl, raycaster, camera, scene, pointer } = useThree();
|
const { gl, raycaster, camera, scene, pointer, size } = useThree();
|
||||||
const { activeTool } = useActiveTool();
|
const { activeTool } = useActiveTool();
|
||||||
const { addComment } = useCommentStore();
|
|
||||||
const [hoverPos, setHoverPos] = useState<Vector3 | null>(null);
|
const [hoverPos, setHoverPos] = useState<Vector3 | null>(null);
|
||||||
|
const { setSelectedComment, setCommentPositionState, setPosition2Dstate } = useSelectedComment();
|
||||||
|
|
||||||
const userId = localStorage.getItem('userId') ?? '';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvasElement = gl.domElement;
|
const canvasElement = gl.domElement;
|
||||||
|
@ -64,7 +63,7 @@ function CommentsGroup() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseClick = () => {
|
const onMouseClick = async () => {
|
||||||
if (drag) return;
|
if (drag) return;
|
||||||
|
|
||||||
const intersects = raycaster
|
const intersects = raycaster
|
||||||
|
@ -83,23 +82,16 @@ function CommentsGroup() {
|
||||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||||
intersect.object.type !== "GridHelper"
|
intersect.object.type !== "GridHelper"
|
||||||
);
|
);
|
||||||
console.log('intersects: ', intersects);
|
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z);
|
const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z);
|
||||||
|
setSelectedComment(null);
|
||||||
|
setCommentPositionState({ position: position.toArray() })
|
||||||
|
|
||||||
const comment: CommentSchema = {
|
|
||||||
state: 'active',
|
|
||||||
commentId: MathUtils.generateUUID(),
|
|
||||||
creatorId: userId,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
comment: '',
|
|
||||||
lastUpdatedAt: new Date().toISOString(),
|
|
||||||
position: position.toArray(),
|
|
||||||
rotation: [0, 0, 0],
|
|
||||||
replies: []
|
|
||||||
}
|
|
||||||
|
|
||||||
addComment(comment);
|
position.project(camera);
|
||||||
|
const x = (position.x * 0.5 + 0.5) * size.width;
|
||||||
|
const y = (-(position.y * 0.5) + 0.5) * size.height;
|
||||||
|
setPosition2Dstate({ x, y })
|
||||||
setHoverPos(null);
|
setHoverPos(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +116,6 @@ function CommentsGroup() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommentInstances />
|
<CommentInstances />
|
||||||
|
|
||||||
{hoverPos && (
|
{hoverPos && (
|
||||||
<Sphere name={'commentHolder'} args={[0.1, 16, 16]} position={hoverPos}>
|
<Sphere name={'commentHolder'} args={[0.1, 16, 16]} position={hoverPos}>
|
||||||
<meshStandardMaterial color="orange" />
|
<meshStandardMaterial color="orange" />
|
||||||
|
|
|
@ -1,64 +1,90 @@
|
||||||
import { Html, TransformControls } from '@react-three/drei';
|
import { Html, TransformControls } from '@react-three/drei';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
|
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||||
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
|
|
||||||
import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads';
|
import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads';
|
||||||
|
import { useSelectedComment } from '../../../../../store/builder/store';
|
||||||
|
import { Group, Object3D, Vector2, Vector3 } from 'three';
|
||||||
|
import { useThree } from '@react-three/fiber';
|
||||||
|
|
||||||
function CommentInstance({ comment }: { comment: CommentSchema }) {
|
function CommentInstance({ comment }: { comment: CommentSchema }) {
|
||||||
const { isPlaying } = usePlayButtonStore();
|
const { isPlaying } = usePlayButtonStore();
|
||||||
const CommentRef = useRef(null);
|
const CommentRef = useRef(null);
|
||||||
const [selectedComment, setSelectedComment] = useState<CommentSchema | null>(null);
|
const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
|
||||||
|
const { selectedComment, setSelectedComment, setPosition2Dstate } = useSelectedComment()
|
||||||
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
|
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
|
||||||
|
const groupRef = useRef<Group>(null);
|
||||||
|
const { size, camera } = useThree();
|
||||||
|
// useEffect(() => {
|
||||||
|
// const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// const keyCombination = detectModifierKeys(e);
|
||||||
|
// if (!selectedComment) return;
|
||||||
|
// if (keyCombination === "G") {
|
||||||
|
// setTransformMode((prev) => (prev === "translate" ? null : "translate"));
|
||||||
|
// }
|
||||||
|
// if (keyCombination === "R") {
|
||||||
|
// setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
useEffect(() => {
|
// window.addEventListener("keydown", handleKeyDown);
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
// return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
const keyCombination = detectModifierKeys(e);
|
// }, [selectedComment]);
|
||||||
if (!selectedComment) return;
|
|
||||||
if (keyCombination === "G") {
|
|
||||||
setTransformMode((prev) => (prev === "translate" ? null : "translate"));
|
|
||||||
}
|
|
||||||
if (keyCombination === "R") {
|
|
||||||
setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [selectedComment]);
|
|
||||||
|
|
||||||
const commentClicked = () => {
|
const commentClicked = () => {
|
||||||
console.log('hii');
|
|
||||||
setSelectedComment(comment);
|
setSelectedComment(comment);
|
||||||
|
const position = new Vector3(comment.position[0], comment.position[1], comment.position[2])
|
||||||
|
|
||||||
|
position.project(camera);
|
||||||
|
const x = (position.x * 0.5 + 0.5) * size.width;
|
||||||
|
const y = (-(position.y * 0.5) + 0.2) * size.height;
|
||||||
|
setPosition2Dstate({ x, y })
|
||||||
|
|
||||||
|
if (groupRef.current) {
|
||||||
|
setSelectedObject(groupRef.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment.state === 'inactive' || isPlaying) return null;
|
useEffect(() => {
|
||||||
|
if (!selectedComment || selectedComment.threadId !== comment.threadId) {
|
||||||
|
setSelectedObject(null)
|
||||||
|
}
|
||||||
|
}, [selectedComment])
|
||||||
|
|
||||||
|
|
||||||
|
if (comment.state === 'inactive' || isPlaying) return null;
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<>
|
<>
|
||||||
|
<group ref={groupRef} position={comment.position} rotation={comment.rotation}>
|
||||||
<Html
|
<Html
|
||||||
ref={CommentRef}
|
ref={CommentRef}
|
||||||
zIndexRange={[1, 0]}
|
zIndexRange={[1, 0]}
|
||||||
prepend
|
prepend
|
||||||
sprite
|
sprite
|
||||||
center
|
center
|
||||||
position={comment.position}
|
// position={comment.position}
|
||||||
rotation={comment.rotation}
|
// rotation={comment.rotation}
|
||||||
className='comments-main-wrapper'
|
className='comments-main-wrapper'
|
||||||
>
|
>
|
||||||
<CommentThreads commentClicked={commentClicked} />
|
<CommentThreads commentClicked={commentClicked} comment={comment} />
|
||||||
</Html>
|
</Html>
|
||||||
{CommentRef.current && transformMode && (
|
</group>
|
||||||
|
{/* {selectedObject && transformMode && (
|
||||||
<TransformControls
|
<TransformControls
|
||||||
object={CommentRef.current}
|
object={selectedObject}
|
||||||
mode={transformMode}
|
mode={transformMode}
|
||||||
onMouseUp={(e) => {
|
onMouseUp={(e) => {
|
||||||
|
console.log("sad");
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommentInstance;
|
export default CommentInstance;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,56 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import CommentInstance from './commentInstance/commentInstance'
|
import CommentInstance from './commentInstance/commentInstance'
|
||||||
import { useCommentStore } from '../../../../store/collaboration/useCommentStore'
|
import { useCommentStore } from '../../../../store/collaboration/useCommentStore'
|
||||||
|
import { getAllThreads } from '../../../../services/factoryBuilder/comments/getAllThreads';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { getUserData } from '../../../../functions/getUserData';
|
||||||
|
import { getRelativeTime } from '../../../../components/ui/collaboration/function/getRelativeTime';
|
||||||
|
|
||||||
|
|
||||||
function CommentInstances() {
|
function CommentInstances() {
|
||||||
const { comments } = useCommentStore();
|
const { comments, setComments } = useCommentStore();
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const { userId } = getUserData()
|
||||||
|
const getThreads = async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
const getComments = await getAllThreads(projectId);
|
||||||
|
|
||||||
|
|
||||||
|
const formattedThreads = Array.isArray(getComments.data)
|
||||||
|
? getComments.data.map((thread: any) => ({
|
||||||
|
...thread,
|
||||||
|
comments: Array.isArray(thread.comments)
|
||||||
|
? thread.comments.map((val: any) => ({
|
||||||
|
replyId: val._id ?? "",
|
||||||
|
creatorId: userId,
|
||||||
|
createdAt: getRelativeTime(val.createdAt),
|
||||||
|
lastUpdatedAt: "1 hr ago",
|
||||||
|
comment: val.comment,
|
||||||
|
_id: val._id ?? "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
// console.log('formattedThreads: ', formattedThreads);
|
||||||
|
setComments(formattedThreads);
|
||||||
|
} catch (err) {
|
||||||
|
// console.error("Failed to fetch threads:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log('comments: ', comments);
|
getThreads();
|
||||||
}, [comments])
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// console.log("comments", comments);
|
||||||
|
}, [comments])
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{comments.map((comment: CommentSchema) => (
|
{comments?.map((comment: CommentSchema) => (
|
||||||
<React.Fragment key={comment.commentId}>
|
<React.Fragment key={comment.threadId}>
|
||||||
<CommentInstance comment={comment} />
|
<CommentInstance comment={comment} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useSelectedComment, useSocketStore } from '../../../store/builder/store';
|
||||||
|
import { useCommentStore } from '../../../store/collaboration/useCommentStore';
|
||||||
|
import { getUserData } from '../../../functions/getUserData';
|
||||||
|
import { getRelativeTime } from '../../../components/ui/collaboration/function/getRelativeTime';
|
||||||
|
|
||||||
|
interface ThreadSocketProps {
|
||||||
|
setMessages: React.Dispatch<React.SetStateAction<Reply[]>>;
|
||||||
|
// mode: 'create' | 'edit' | null
|
||||||
|
modeRef: React.RefObject<'create' | 'edit' | null>;
|
||||||
|
messages: Reply[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThreadSocketResponsesDev = ({ setMessages, modeRef, messages }: ThreadSocketProps) => {
|
||||||
|
const { threadSocket } = useSocketStore();
|
||||||
|
const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState } = useSelectedComment();
|
||||||
|
const { comments, addComment, addReply, updateComment, updateReply } = useCommentStore();
|
||||||
|
const { userId } = getUserData();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!threadSocket) return;
|
||||||
|
|
||||||
|
// --- Add Comment Handler ---
|
||||||
|
// const handleAddComment = (data: any) => {
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// const commentData = {
|
||||||
|
// replyId: data.data?._id,
|
||||||
|
// creatorId: data.data?.userId,
|
||||||
|
// createdAt: getRelativeTime(data.data?.createdAt),
|
||||||
|
// lastUpdatedAt: '2 hrs ago',
|
||||||
|
// comment: data.data.comment,
|
||||||
|
// };
|
||||||
|
// //
|
||||||
|
//
|
||||||
|
// if (mode == "create") {
|
||||||
|
// addReply(selectedComment?.threadId, commentData);
|
||||||
|
//
|
||||||
|
// setMessages(prevMessages => [...prevMessages, commentData]);
|
||||||
|
// } else if (mode == "edit") {
|
||||||
|
// updateReply(selectedComment?.threadId, data.data?._id, commentData);
|
||||||
|
// setMessages((prev) =>
|
||||||
|
// prev.map((message) => {
|
||||||
|
// // 👈 log each message
|
||||||
|
// return (message.replyId || message._id) === data.data?._id
|
||||||
|
// ? { ...message, comment: data.data?.comment }
|
||||||
|
// : message;
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// } else {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// threadSocket.off('v1-Comment:response:add', handleAddComment);
|
||||||
|
// };
|
||||||
|
// threadSocket.on('v1-Comment:response:add', handleAddComment);
|
||||||
|
const handleAddComment = (data: any) => {
|
||||||
|
// console.log('Add: ', data);
|
||||||
|
|
||||||
|
const commentData = {
|
||||||
|
replyId: data.data?._id,
|
||||||
|
creatorId: data.data?.userId,
|
||||||
|
createdAt: getRelativeTime(data.data?.createdAt),
|
||||||
|
lastUpdatedAt: "2 hrs ago",
|
||||||
|
comment: data.data.comment,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (modeRef.current === "create") {
|
||||||
|
|
||||||
|
|
||||||
|
addReply(selectedComment?.threadId, commentData);
|
||||||
|
setMessages((prevMessages) => [...prevMessages, commentData]);
|
||||||
|
} else if (modeRef.current === "edit") {
|
||||||
|
|
||||||
|
updateReply(selectedComment?.threadId, data.data?._id, commentData);
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((message) => {
|
||||||
|
// 👈 log each message
|
||||||
|
return message.replyId === data.data?._id
|
||||||
|
? { ...message, comment: data.data?.comment }
|
||||||
|
: message;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// console.log('data.data?.comment: ', data.data?.comment);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
threadSocket.off("v1-Comment:response:add", handleAddComment);
|
||||||
|
};
|
||||||
|
threadSocket.on('v1-Comment:response:add', handleAddComment);
|
||||||
|
// --- Delete Comment Handler ---
|
||||||
|
|
||||||
|
const handleDeleteComment = (data: any) => {
|
||||||
|
// console.log('delete: ', data);
|
||||||
|
|
||||||
|
threadSocket.off('v1-Comment:response:delete', handleDeleteComment);
|
||||||
|
};
|
||||||
|
threadSocket.on('v1-Comment:response:delete', handleDeleteComment);
|
||||||
|
|
||||||
|
// --- Create Thread Handler ---
|
||||||
|
const handleCreateThread = (data: any) => {
|
||||||
|
// console.log('createThread: ', data);
|
||||||
|
|
||||||
|
const comment: CommentSchema = {
|
||||||
|
state: 'active',
|
||||||
|
threadId: data.data?._id,
|
||||||
|
creatorId: userId || data.data?.userId,
|
||||||
|
createdAt: data.data?.createdAt,
|
||||||
|
threadTitle: data.data?.threadTitle,
|
||||||
|
lastUpdatedAt: new Date().toISOString(),
|
||||||
|
position: commentPositionState.position,
|
||||||
|
rotation: [0, 0, 0],
|
||||||
|
comments: [],
|
||||||
|
};
|
||||||
|
setSelectedComment(comment)
|
||||||
|
addComment(comment);
|
||||||
|
setCommentPositionState(null);
|
||||||
|
// setSelectedComment(null);
|
||||||
|
threadSocket.off('v1-thread:response:create', handleCreateThread);
|
||||||
|
};
|
||||||
|
threadSocket.on('v1-thread:response:create', handleCreateThread);
|
||||||
|
|
||||||
|
// --- Delete Thread Handler ---
|
||||||
|
const handleDeleteThread = (data: any) => {
|
||||||
|
|
||||||
|
threadSocket.off('v1-thread:response:delete', handleDeleteThread);
|
||||||
|
};
|
||||||
|
threadSocket.on('v1-thread:response:delete', handleDeleteThread);
|
||||||
|
|
||||||
|
|
||||||
|
const handleEditThread = (data: any) => {
|
||||||
|
|
||||||
|
const editedThread: CommentSchema = {
|
||||||
|
state: 'active',
|
||||||
|
threadId: data.data?._id,
|
||||||
|
creatorId: userId,
|
||||||
|
createdAt: data.data?.createdAt,
|
||||||
|
threadTitle: data.data?.threadTitle,
|
||||||
|
lastUpdatedAt: new Date().toISOString(),
|
||||||
|
position: data.data.position,
|
||||||
|
rotation: [0, 0, 0],
|
||||||
|
comments: data.data.comments,
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log('data.data?._id: ', data.data?._id);
|
||||||
|
updateComment(data.data?._id, editedThread);
|
||||||
|
setSelectedComment(editedThread)
|
||||||
|
|
||||||
|
// setSelectedComment(null);
|
||||||
|
|
||||||
|
};
|
||||||
|
threadSocket.on('v1-thread:response:updateTitle', handleEditThread);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
threadSocket.off('v1-Comment:response:add', handleAddComment);
|
||||||
|
threadSocket.off('v1-Comment:response:delete', handleDeleteComment);
|
||||||
|
threadSocket.off('v1-thread:response:create', handleCreateThread);
|
||||||
|
threadSocket.off('v1-thread:response:delete', handleDeleteThread);
|
||||||
|
threadSocket.off('v1-thread:response:updateTitle', handleEditThread);
|
||||||
|
};
|
||||||
|
}, [threadSocket, selectedComment, commentPositionState, userId, setMessages, addReply, addComment, setSelectedComment, setCommentPositionState, updateComment, comments, modeRef.current, messages]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThreadSocketResponsesDev;
|
|
@ -6,8 +6,12 @@ import {
|
||||||
useUserName,
|
useUserName,
|
||||||
useWallItems,
|
useWallItems,
|
||||||
useSaveVersion,
|
useSaveVersion,
|
||||||
|
useViewSceneStore,
|
||||||
useProjectName,
|
useProjectName,
|
||||||
|
useRenameModeStore,
|
||||||
|
useSelectedFloorItem,
|
||||||
useZones,
|
useZones,
|
||||||
|
useSelectedComment,
|
||||||
} from "../store/builder/store";
|
} from "../store/builder/store";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useSelectedUserStore } from "../store/collaboration/useCollabStore";
|
import { useSelectedUserStore } from "../store/collaboration/useCollabStore";
|
||||||
|
@ -26,6 +30,7 @@ import { SceneProvider } from "../modules/scene/sceneContext";
|
||||||
import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/getVersionHistoryApi";
|
import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/getVersionHistoryApi";
|
||||||
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
|
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
|
||||||
import { VersionProvider } from "../modules/builder/version/versionContext";
|
import { VersionProvider } from "../modules/builder/version/versionContext";
|
||||||
|
import ThreadChat from "../components/ui/collaboration/ThreadChat";
|
||||||
|
|
||||||
const Project: React.FC = () => {
|
const Project: React.FC = () => {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
@ -44,6 +49,7 @@ const Project: React.FC = () => {
|
||||||
const { selectedUser } = useSelectedUserStore();
|
const { selectedUser } = useSelectedUserStore();
|
||||||
const { isLogListVisible } = useLogger();
|
const { isLogListVisible } = useLogger();
|
||||||
const { setVersions } = useVersionHistoryStore();
|
const { setVersions } = useVersionHistoryStore();
|
||||||
|
const { selectedComment, setSelectedComment, commentPositionState } = useSelectedComment();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!email || !userId) {
|
if (!email || !userId) {
|
||||||
|
@ -124,6 +130,7 @@ const Project: React.FC = () => {
|
||||||
<LogList />
|
<LogList />
|
||||||
</RenderOverlay>
|
</RenderOverlay>
|
||||||
)}
|
)}
|
||||||
|
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
|
||||||
|
|
||||||
|
export const addCommentsApi = async (
|
||||||
|
projectId: any,
|
||||||
|
comment: string,
|
||||||
|
threadId?: string,
|
||||||
|
commentId?: string
|
||||||
|
) => {
|
||||||
|
console.log(
|
||||||
|
" projectId, comments, threadId: ",
|
||||||
|
projectId,
|
||||||
|
comment,
|
||||||
|
threadId,
|
||||||
|
commentId
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${url_Backend_dwinzo}/api/v1/Thread/addComment`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer <access_token>",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
token: localStorage.getItem("token") || "",
|
||||||
|
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ projectId, comment, threadId, commentId }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('response: ', response);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to add project");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log("result: ", result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error("An unknown error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,34 @@
|
||||||
|
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
|
||||||
|
|
||||||
|
export const createThreadApi = async (
|
||||||
|
projectId: any,
|
||||||
|
state: string,
|
||||||
|
position: any,
|
||||||
|
rotation: any,
|
||||||
|
threadTitle: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${url_Backend_dwinzo}/api/v1/upsetThread`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer <access_token>",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
token: localStorage.getItem("token") || "",
|
||||||
|
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ projectId, state, position, rotation, threadTitle }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to add project");
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('result: ', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error("An unknown error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
|
||||||
|
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
|
||||||
|
|
||||||
|
export const deleteCommentApi = async (
|
||||||
|
projectId: string,
|
||||||
|
threadId: string,
|
||||||
|
commentId: string
|
||||||
|
) => {
|
||||||
|
const body: any = {
|
||||||
|
projectId,
|
||||||
|
threadId,
|
||||||
|
commentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${url_Backend_dwinzo}/api/v1/Thread/deleteComment`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer <access_token>",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
token: localStorage.getItem("token") || "",
|
||||||
|
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to clearPanel in the zone");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error("An unknown error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
|
||||||
|
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
|
||||||
|
|
||||||
|
export const deleteThreadApi = async (projectId: string, threadId: string) => {
|
||||||
|
const body: any = {
|
||||||
|
projectId,
|
||||||
|
threadId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${url_Backend_dwinzo}/api/v1/Thread/delete`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer <access_token>",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
token: localStorage.getItem("token") || "",
|
||||||
|
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to clearPanel in the zone");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error("An unknown error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
|
||||||
|
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
|
||||||
|
|
||||||
|
export const editThreadTitleApi = async (
|
||||||
|
projectId: string,
|
||||||
|
threadId: string,
|
||||||
|
threadTitle: string
|
||||||
|
) => {
|
||||||
|
const body: any = {
|
||||||
|
projectId,
|
||||||
|
threadId,
|
||||||
|
threadTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${url_Backend_dwinzo}/api/v1/Thread/updateTitle`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer <access_token>",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
token: localStorage.getItem("token") || "",
|
||||||
|
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to clearPanel in the zone");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error("An unknown error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,34 @@
|
||||||
|
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
|
||||||
|
|
||||||
|
export const getAllThreads = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${url_Backend_dwinzo}/api/v1/Threads/${projectId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer <access_token>",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
token: localStorage.getItem("token") || "",
|
||||||
|
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to get assets");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
// console.log('result: ', result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
echo.error("Failed to get floor asset");
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error("An unknown error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -27,7 +27,6 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const dashBoardSocket = io(
|
const dashBoardSocket = io(
|
||||||
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/dashboard`,
|
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/dashboard`,
|
||||||
{
|
{
|
||||||
|
@ -42,8 +41,21 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
|
||||||
auth: { token },
|
auth: { token },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const threadSocket = io(
|
||||||
|
`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`,
|
||||||
|
{
|
||||||
|
reconnection: true,
|
||||||
|
auth: { token },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
set({ socket, visualizationSocket, dashBoardSocket, projectSocket });
|
set({
|
||||||
|
socket,
|
||||||
|
visualizationSocket,
|
||||||
|
dashBoardSocket,
|
||||||
|
projectSocket,
|
||||||
|
threadSocket,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
disconnectSocket: () => {
|
disconnectSocket: () => {
|
||||||
set((state: any) => {
|
set((state: any) => {
|
||||||
|
@ -51,6 +63,7 @@ export const useSocketStore = create<any>((set: any, get: any) => ({
|
||||||
state.visualizationSocket?.disconnect();
|
state.visualizationSocket?.disconnect();
|
||||||
state.dashBoardSocket?.disconnect();
|
state.dashBoardSocket?.disconnect();
|
||||||
state.projectSocket?.disconnect();
|
state.projectSocket?.disconnect();
|
||||||
|
state.threadSocket?.disconnect();
|
||||||
return { socket: null };
|
return { socket: null };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -657,10 +670,10 @@ export const useViewSceneStore = create<ViewSceneState>((set) => ({
|
||||||
setViewSceneLabels: (value) => {
|
setViewSceneLabels: (value) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newValue =
|
const newValue =
|
||||||
typeof value === 'function' ? value(state.viewSceneLabels) : value;
|
typeof value === "function" ? value(state.viewSceneLabels) : value;
|
||||||
|
|
||||||
// Store in localStorage manually
|
// Store in localStorage manually
|
||||||
localStorage.setItem('viewSceneLabels', JSON.stringify(newValue));
|
localStorage.setItem("viewSceneLabels", JSON.stringify(newValue));
|
||||||
|
|
||||||
return { viewSceneLabels: newValue };
|
return { viewSceneLabels: newValue };
|
||||||
});
|
});
|
||||||
|
@ -668,8 +681,8 @@ export const useViewSceneStore = create<ViewSceneState>((set) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function getInitialViewSceneLabels(): boolean {
|
function getInitialViewSceneLabels(): boolean {
|
||||||
if (typeof window === 'undefined') return false; // SSR safety
|
if (typeof window === "undefined") return false; // SSR safety
|
||||||
const saved = localStorage.getItem('viewSceneLabels');
|
const saved = localStorage.getItem("viewSceneLabels");
|
||||||
return saved ? JSON.parse(saved) : false;
|
return saved ? JSON.parse(saved) : false;
|
||||||
}
|
}
|
||||||
export interface CompareProduct {
|
export interface CompareProduct {
|
||||||
|
@ -691,7 +704,7 @@ export interface CompareProduct {
|
||||||
machineIdleTime: number;
|
machineIdleTime: number;
|
||||||
machineActiveTime: number;
|
machineActiveTime: number;
|
||||||
throughputData: number;
|
throughputData: number;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCompareProductDataStore = create<{
|
export const useCompareProductDataStore = create<{
|
||||||
|
@ -701,3 +714,12 @@ export const useCompareProductDataStore = create<{
|
||||||
compareProductsData: [],
|
compareProductsData: [],
|
||||||
setCompareProductsData: (x) => set({ compareProductsData: x }),
|
setCompareProductsData: (x) => set({ compareProductsData: x }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const useSelectedComment = create<any>((set: any) => ({
|
||||||
|
selectedComment: null,
|
||||||
|
setSelectedComment: (x: any) => set({ selectedComment: x }),
|
||||||
|
position2Dstate: {},
|
||||||
|
setPosition2Dstate: (x: any) => set({ position2Dstate: x }),
|
||||||
|
commentPositionState: null,
|
||||||
|
setCommentPositionState: (x: any) => set({ commentPositionState: x }),
|
||||||
|
}));
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { create } from 'zustand';
|
import { create } from "zustand";
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
interface CommentStore {
|
interface CommentStore {
|
||||||
comments: CommentsSchema;
|
comments: CommentsSchema;
|
||||||
|
@ -7,16 +7,20 @@ interface CommentStore {
|
||||||
// Comment operations
|
// Comment operations
|
||||||
addComment: (comment: CommentSchema) => void;
|
addComment: (comment: CommentSchema) => void;
|
||||||
setComments: (comments: CommentsSchema) => void;
|
setComments: (comments: CommentsSchema) => void;
|
||||||
updateComment: (commentId: string, updates: Partial<CommentSchema>) => void;
|
updateComment: (threadId: string, updates: Partial<CommentSchema>) => void;
|
||||||
removeComment: (commentId: string) => void;
|
removeComment: (threadId: string) => void;
|
||||||
|
|
||||||
// Reply operations
|
// Reply operations
|
||||||
addReply: (commentId: string, reply: Reply) => void;
|
addReply: (threadId: string, reply: Reply) => void;
|
||||||
updateReply: (commentId: string, replyId: string, updates: Partial<Reply>) => void;
|
updateReply: (
|
||||||
removeReply: (commentId: string, replyId: string) => void;
|
threadId: string,
|
||||||
|
replyId: string,
|
||||||
|
updates: Partial<Reply>
|
||||||
|
) => void;
|
||||||
|
removeReply: (threadId: string, _id: string) => void;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
getCommentById: (commentId: string) => CommentSchema | undefined;
|
getCommentById: (threadId: string) => CommentSchema | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommentStore = create<CommentStore>()(
|
export const useCommentStore = create<CommentStore>()(
|
||||||
|
@ -26,7 +30,7 @@ export const useCommentStore = create<CommentStore>()(
|
||||||
// Comment operations
|
// Comment operations
|
||||||
addComment: (comment) => {
|
addComment: (comment) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (!state.comments.find(c => c.commentId === comment.commentId)) {
|
if (!state.comments.find((c) => c.threadId === comment.threadId)) {
|
||||||
state.comments.push(JSON.parse(JSON.stringify(comment)));
|
state.comments.push(JSON.parse(JSON.stringify(comment)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -38,55 +42,56 @@ export const useCommentStore = create<CommentStore>()(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateComment: (commentId, updates) => {
|
updateComment: (threadId, updates) => {
|
||||||
|
console.log("threadId:updater ", threadId);
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const comment = state.comments.find(c => c.commentId === commentId);
|
const comment = state.comments.find((c) => c.threadId === threadId);
|
||||||
if (comment) {
|
if (comment) {
|
||||||
Object.assign(comment, updates);
|
Object.assign(comment, updates);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeComment: (commentId) => {
|
removeComment: (threadId) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.comments = state.comments.filter(c => c.commentId !== commentId);
|
state.comments = state.comments.filter((c) => c.threadId !== threadId);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Reply operations
|
// Reply operations
|
||||||
addReply: (commentId, reply) => {
|
addReply: (threadId, comment) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const comment = state.comments.find(c => c.commentId === commentId);
|
const reply = state.comments.find((c) => c.threadId === threadId);
|
||||||
if (comment) {
|
|
||||||
comment.replies.push(reply);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateReply: (commentId, replyId, updates) => {
|
|
||||||
set((state) => {
|
|
||||||
const comment = state.comments.find(c => c.commentId === commentId);
|
|
||||||
if (comment) {
|
|
||||||
const reply = comment.replies.find(r => r.replyId === replyId);
|
|
||||||
if (reply) {
|
if (reply) {
|
||||||
Object.assign(reply, updates);
|
reply.comments.push(comment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReply: (threadId, replyId, updates) => {
|
||||||
|
set((state) => {
|
||||||
|
const reply = state.comments.find((c) => c.threadId === threadId);
|
||||||
|
if (reply) {
|
||||||
|
const comment = reply.comments.find((r) => r.replyId === replyId);
|
||||||
|
if (comment) {
|
||||||
|
Object.assign(comment, updates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeReply: (commentId, replyId) => {
|
removeReply: (threadId, _id) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const comment = state.comments.find(c => c.commentId === commentId);
|
const comment = state.comments.find((c) => c.threadId === threadId);
|
||||||
if (comment) {
|
if (comment) {
|
||||||
comment.replies = comment.replies.filter(r => r.replyId !== replyId);
|
comment.comments = comment.comments.filter((r) => r.replyId !== _id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Getter
|
// Getter
|
||||||
getCommentById: (commentId) => {
|
getCommentById: (threadId) => {
|
||||||
return get().comments.find(c => c.commentId === commentId);
|
return get().comments.find((c) => c.threadId === threadId);
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
|
@ -56,16 +56,23 @@
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.replies {
|
.replies,
|
||||||
|
.comments {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
color: var(--input-text-color);
|
color: var(--input-text-color);
|
||||||
}
|
}
|
||||||
.header,
|
.header,
|
||||||
.message,
|
.message,
|
||||||
.replies {
|
.replies,
|
||||||
|
.comments {
|
||||||
display: none;
|
display: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
@ -84,9 +91,15 @@
|
||||||
.users-commented {
|
.users-commented {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
.message {
|
||||||
|
display: -webkit-box !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.header,
|
.header,
|
||||||
.message,
|
.replies,
|
||||||
.replies {
|
.comments {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
@ -158,6 +171,8 @@
|
||||||
.messages-wrapper {
|
.messages-wrapper {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
.edit-container {
|
.edit-container {
|
||||||
.input-container {
|
.input-container {
|
||||||
textarea {
|
textarea {
|
||||||
|
@ -284,7 +299,9 @@
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
svg {
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
rotate: 45deg;
|
rotate: 45deg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
interface CommentSchema {
|
interface CommentSchema {
|
||||||
state: "active" | "inactive";
|
state: "active" | "inactive";
|
||||||
commentId: string;
|
threadId: string;
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
comment: string;
|
threadTitle: string;
|
||||||
lastUpdatedAt: string;
|
lastUpdatedAt: string;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
rotation: [number, number, number];
|
rotation: [number, number, number];
|
||||||
replies: Reply[];
|
comments: Reply[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Reply {
|
interface Reply {
|
||||||
|
@ -15,7 +15,7 @@ interface Reply {
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastUpdatedAt: string;
|
lastUpdatedAt: string;
|
||||||
reply: string;
|
comment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommentsSchema = CommentSchema[];
|
type CommentsSchema = CommentSchema[];
|
||||||
|
|
|
@ -7,6 +7,7 @@ import useVersionHistoryVisibleStore, {
|
||||||
useAddAction,
|
useAddAction,
|
||||||
useRenameModeStore,
|
useRenameModeStore,
|
||||||
useSaveVersion,
|
useSaveVersion,
|
||||||
|
useSelectedComment,
|
||||||
useSelectedFloorItem,
|
useSelectedFloorItem,
|
||||||
useSelectedWallItem,
|
useSelectedWallItem,
|
||||||
useShortcutStore,
|
useShortcutStore,
|
||||||
|
@ -47,6 +48,7 @@ const KeyPressListener: React.FC = () => {
|
||||||
const { selectedFloorItem } = useSelectedFloorItem();
|
const { selectedFloorItem } = useSelectedFloorItem();
|
||||||
const { setCreateNewVersion } = useVersionHistoryStore();
|
const { setCreateNewVersion } = useVersionHistoryStore();
|
||||||
const { setVersionHistoryVisible } = useVersionHistoryVisibleStore();
|
const { setVersionHistoryVisible } = useVersionHistoryVisibleStore();
|
||||||
|
const { setSelectedComment } = useSelectedComment();
|
||||||
|
|
||||||
const isTextInput = (element: Element | null): boolean =>
|
const isTextInput = (element: Element | null): boolean =>
|
||||||
element instanceof HTMLInputElement ||
|
element instanceof HTMLInputElement ||
|
||||||
|
@ -193,6 +195,7 @@ const KeyPressListener: React.FC = () => {
|
||||||
clearComparisonProduct();
|
clearComparisonProduct();
|
||||||
setIsLogListVisible(false);
|
setIsLogListVisible(false);
|
||||||
setIsRenameMode(false);
|
setIsRenameMode(false);
|
||||||
|
setSelectedComment(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
Loading…
Reference in New Issue