comment fixing
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ app.config.timestamp_*.js
|
|||||||
*.launch
|
*.launch
|
||||||
.settings/
|
.settings/
|
||||||
tasks
|
tasks
|
||||||
|
main.go
|
||||||
|
|
||||||
# Temp
|
# Temp
|
||||||
gitignore
|
gitignore
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ export function LeftBar() {
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-label="Main navigation"
|
aria-label="Main navigation"
|
||||||
class="border-r-overlay2 bg-base fixed z-9999 h-dvh border-r-2 transition-transform duration-500 ease-out"
|
class="border-r-overlay2 bg-base fixed z-200 h-dvh border-r-2 transition-transform duration-500 ease-out"
|
||||||
classList={{
|
classList={{
|
||||||
"-translate-x-full": !leftBarVisible(),
|
"-translate-x-full": !leftBarVisible(),
|
||||||
"translate-x-0": leftBarVisible()
|
"translate-x-0": leftBarVisible()
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ export default function CommentBlock(props: CommentBlockProps) {
|
|||||||
|
|
||||||
const deleteCommentTrigger = async (e: MouseEvent) => {
|
const deleteCommentTrigger = async (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log("Delete comment");
|
|
||||||
|
|
||||||
setDeletionLoading(true);
|
setDeletionLoading(true);
|
||||||
const user = userData();
|
const user = userData();
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function CommentDeletionPrompt(
|
|||||||
open={props.isOpen}
|
open={props.isOpen}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
title="Comment Deletion"
|
title="Comment Deletion"
|
||||||
class="bg-red brightness-110"
|
class="bg-crust brightness-110"
|
||||||
>
|
>
|
||||||
<div class="bg-surface0 mx-auto w-3/4 rounded px-6 py-4">
|
<div class="bg-surface0 mx-auto w-3/4 rounded px-6 py-4">
|
||||||
<div class="flex overflow-x-auto overflow-y-hidden select-text">
|
<div class="flex overflow-x-auto overflow-y-hidden select-text">
|
||||||
@@ -99,9 +99,7 @@ export default function CommentDeletionPrompt(
|
|||||||
checked={adminDeleteChecked()}
|
checked={adminDeleteChecked()}
|
||||||
onChange={handleAdminDeleteCheckbox}
|
onChange={handleAdminDeleteCheckbox}
|
||||||
/>
|
/>
|
||||||
<div class="my-auto px-2 text-sm font-normal">
|
<div class="my-auto px-2 text-sm font-normal">Admin Delete?</div>
|
||||||
Confirm Admin Delete?
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-center">
|
<div class="flex w-full justify-center">
|
||||||
@@ -112,9 +110,7 @@ export default function CommentDeletionPrompt(
|
|||||||
checked={fullDeleteChecked()}
|
checked={fullDeleteChecked()}
|
||||||
onChange={handleFullDeleteCheckbox}
|
onChange={handleFullDeleteCheckbox}
|
||||||
/>
|
/>
|
||||||
<div class="my-auto px-2 text-sm font-normal">
|
<div class="my-auto px-2 text-sm font-normal">Database Delete?</div>
|
||||||
Confirm Full Delete (removal from database)?
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import { env } from "~/env/client";
|
|||||||
|
|
||||||
const MAX_RETRIES = 12;
|
const MAX_RETRIES = 12;
|
||||||
const RETRY_INTERVAL = 5000;
|
const RETRY_INTERVAL = 5000;
|
||||||
|
const OPERATION_TIMEOUT = 10000; // 10 seconds timeout for operations
|
||||||
|
|
||||||
|
type ConnectionState = "disconnected" | "connecting" | "connected" | "error";
|
||||||
|
|
||||||
export default function CommentSectionWrapper(
|
export default function CommentSectionWrapper(
|
||||||
props: CommentSectionWrapperProps
|
props: CommentSectionWrapperProps
|
||||||
@@ -55,43 +58,106 @@ export default function CommentSectionWrapper(
|
|||||||
] = createSignal<string | undefined>(undefined);
|
] = createSignal<string | undefined>(undefined);
|
||||||
const [commentBodyForModification, setCommentBodyForModification] =
|
const [commentBodyForModification, setCommentBodyForModification] =
|
||||||
createSignal<string>("");
|
createSignal<string>("");
|
||||||
|
const [operationError, setOperationError] = createSignal<string>("");
|
||||||
let userCommentMap: Map<UserPublicData, number[]> = props.userCommentMap;
|
const [connectionState, setConnectionState] =
|
||||||
|
createSignal<ConnectionState>("disconnected");
|
||||||
|
const [userCommentMap, setUserCommentMap] = createSignal<
|
||||||
|
Map<UserPublicData, number[]>
|
||||||
|
>(props.userCommentMap);
|
||||||
let deletePromptRef: HTMLDivElement | undefined;
|
let deletePromptRef: HTMLDivElement | undefined;
|
||||||
let modificationPromptRef: HTMLDivElement | undefined;
|
let modificationPromptRef: HTMLDivElement | undefined;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
let socket: WebSocket | undefined;
|
let socket: WebSocket | undefined;
|
||||||
|
let commentSubmitTimeoutId: number | undefined;
|
||||||
|
let editCommentTimeoutId: number | undefined;
|
||||||
|
let deleteCommentTimeoutId: number | undefined;
|
||||||
|
let reconnectTimeoutId: number | undefined;
|
||||||
|
let isMounted = true;
|
||||||
|
let intentionalDisconnect = false;
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
if (socket) return;
|
// Don't connect if not mounted or intentionally disconnected
|
||||||
|
if (!isMounted || intentionalDisconnect) {
|
||||||
|
console.log(
|
||||||
|
"[WebSocket] Skipping connection: component unmounted or intentional disconnect"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
console.log("[WebSocket] Socket already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (retryCount > MAX_RETRIES) {
|
if (retryCount > MAX_RETRIES) {
|
||||||
console.error("Max retries exceeded!");
|
console.error("[WebSocket] Max retries exceeded!");
|
||||||
|
setConnectionState("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate we have required data before connecting
|
||||||
|
if (!props.id) {
|
||||||
|
console.warn("[WebSocket] No post ID available, skipping connection");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const websocketUrl = env.VITE_WEBSOCKET;
|
const websocketUrl = env.VITE_WEBSOCKET;
|
||||||
if (!websocketUrl) {
|
if (!websocketUrl) {
|
||||||
console.error("VITE_WEBSOCKET environment variable not set");
|
console.error(
|
||||||
|
"[WebSocket] VITE_WEBSOCKET environment variable not set"
|
||||||
|
);
|
||||||
|
setConnectionState("error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[WebSocket] Connecting... (attempt ${retryCount + 1}/${MAX_RETRIES + 1})`
|
||||||
|
);
|
||||||
|
setConnectionState("connecting");
|
||||||
|
|
||||||
const newSocket = new WebSocket(websocketUrl);
|
const newSocket = new WebSocket(websocketUrl);
|
||||||
|
|
||||||
newSocket.onopen = () => {
|
newSocket.onopen = () => {
|
||||||
|
console.log("[WebSocket] Connected successfully");
|
||||||
|
setConnectionState("connected");
|
||||||
updateChannel();
|
updateChannel();
|
||||||
retryCount = 0;
|
retryCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
newSocket.onclose = () => {
|
newSocket.onclose = (event) => {
|
||||||
retryCount += 1;
|
console.log(
|
||||||
|
`[WebSocket] Connection closed (code: ${event.code}, reason: ${event.reason || "none"})`
|
||||||
|
);
|
||||||
socket = undefined;
|
socket = undefined;
|
||||||
setTimeout(connect, RETRY_INTERVAL);
|
setConnectionState("disconnected");
|
||||||
|
|
||||||
|
// Only retry if still mounted and not intentional disconnect
|
||||||
|
if (isMounted && !intentionalDisconnect && retryCount <= MAX_RETRIES) {
|
||||||
|
retryCount += 1;
|
||||||
|
console.log(
|
||||||
|
`[WebSocket] Scheduling reconnect in ${RETRY_INTERVAL}ms (attempt ${retryCount}/${MAX_RETRIES + 1})`
|
||||||
|
);
|
||||||
|
reconnectTimeoutId = window.setTimeout(connect, RETRY_INTERVAL);
|
||||||
|
} else {
|
||||||
|
console.log("[WebSocket] Not reconnecting:", {
|
||||||
|
isMounted,
|
||||||
|
intentionalDisconnect,
|
||||||
|
retryCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
newSocket.onerror = (error) => {
|
||||||
|
console.error("[WebSocket] Connection error:", error);
|
||||||
|
setConnectionState("error");
|
||||||
};
|
};
|
||||||
|
|
||||||
newSocket.onmessage = (messageEvent) => {
|
newSocket.onmessage = (messageEvent) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(messageEvent.data) as WebSocketBroadcast;
|
const parsed = JSON.parse(messageEvent.data) as WebSocketBroadcast;
|
||||||
|
console.log("[WebSocket] Message received:", parsed.action);
|
||||||
|
|
||||||
switch (parsed.action) {
|
switch (parsed.action) {
|
||||||
case "commentCreationBroadcast":
|
case "commentCreationBroadcast":
|
||||||
createCommentHandler(parsed);
|
createCommentHandler(parsed);
|
||||||
@@ -106,10 +172,11 @@ export default function CommentSectionWrapper(
|
|||||||
commentReactionHandler(parsed);
|
commentReactionHandler(parsed);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
console.log("[WebSocket] Unknown action:", parsed.action);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error("[WebSocket] Error parsing message:", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,20 +186,58 @@ export default function CommentSectionWrapper(
|
|||||||
connect();
|
connect();
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (socket?.readyState === WebSocket.OPEN) {
|
console.log("[WebSocket] Component cleanup starting");
|
||||||
socket.close();
|
isMounted = false;
|
||||||
socket = undefined;
|
intentionalDisconnect = true;
|
||||||
|
|
||||||
|
// Clear reconnect timeout
|
||||||
|
if (reconnectTimeoutId) {
|
||||||
|
clearTimeout(reconnectTimeoutId);
|
||||||
|
reconnectTimeoutId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send disconnect message if connected
|
||||||
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
action: "disconnect",
|
||||||
|
postType: "blog",
|
||||||
|
postID: props.id,
|
||||||
|
invokerID: props.currentUserID
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log("[WebSocket] Disconnect message sent");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WebSocket] Error sending disconnect message:", error);
|
||||||
|
}
|
||||||
|
socket.close(1000, "Component unmounted");
|
||||||
|
} else if (socket) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = undefined;
|
||||||
|
setConnectionState("disconnected");
|
||||||
|
|
||||||
|
// Clear operation timeouts
|
||||||
|
if (commentSubmitTimeoutId) clearTimeout(commentSubmitTimeoutId);
|
||||||
|
if (editCommentTimeoutId) clearTimeout(editCommentTimeoutId);
|
||||||
|
if (deleteCommentTimeoutId) clearTimeout(deleteCommentTimeoutId);
|
||||||
|
|
||||||
|
console.log("[WebSocket] Component cleanup complete");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateChannel = () => {
|
const updateChannel = () => {
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn("[WebSocket] Cannot update channel: socket not ready");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.currentUserID || !props.id) {
|
if (!props.currentUserID || !props.id) {
|
||||||
console.warn("Cannot update channel: missing userID or postID");
|
console.warn(
|
||||||
|
"[WebSocket] Cannot update channel: missing userID or postID"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,24 +247,38 @@ export default function CommentSectionWrapper(
|
|||||||
action: "channelUpdate",
|
action: "channelUpdate",
|
||||||
postType: "blog",
|
postType: "blog",
|
||||||
postID: props.id,
|
postID: props.id,
|
||||||
invoker_id: props.currentUserID
|
invokerID: props.currentUserID
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
console.log(`[WebSocket] Channel updated for post ${props.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending channel update:", error);
|
console.error("[WebSocket] Error sending channel update:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newComment = async (commentBody: string, parentCommentID?: number) => {
|
const newComment = async (commentBody: string, parentCommentID?: number) => {
|
||||||
setCommentSubmitLoading(true);
|
setCommentSubmitLoading(true);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (commentSubmitTimeoutId) {
|
||||||
|
clearTimeout(commentSubmitTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout to clear loading state
|
||||||
|
commentSubmitTimeoutId = window.setTimeout(() => {
|
||||||
|
console.warn("Comment submission timed out");
|
||||||
|
setCommentSubmitLoading(false);
|
||||||
|
setOperationError("Comment submission timed out. Please try again.");
|
||||||
|
}, OPERATION_TIMEOUT);
|
||||||
|
|
||||||
if (!props.currentUserID) {
|
if (!props.currentUserID) {
|
||||||
console.warn("Cannot create comment: user not authenticated");
|
console.warn("Cannot create comment: user not authenticated");
|
||||||
|
clearTimeout(commentSubmitTimeoutId);
|
||||||
setCommentSubmitLoading(false);
|
setCommentSubmitLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commentBody && socket) {
|
if (commentBody && socket && socket.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -173,9 +292,11 @@ export default function CommentSectionWrapper(
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending comment creation:", error);
|
console.error("Error sending comment creation:", error);
|
||||||
|
clearTimeout(commentSubmitTimeoutId);
|
||||||
await fallbackCommentCreation(commentBody, parentCommentID);
|
await fallbackCommentCreation(commentBody, parentCommentID);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
clearTimeout(commentSubmitTimeoutId);
|
||||||
await fallbackCommentCreation(commentBody, parentCommentID);
|
await fallbackCommentCreation(commentBody, parentCommentID);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -205,9 +326,15 @@ export default function CommentSectionWrapper(
|
|||||||
commenterID: props.currentUserID,
|
commenterID: props.currentUserID,
|
||||||
commentParent: parentCommentID
|
commentParent: parentCommentID
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to create comment");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in fallback comment creation:", error);
|
console.error("Error in fallback comment creation:", error);
|
||||||
|
setOperationError("Failed to post comment. Please try again.");
|
||||||
|
if (commentSubmitTimeoutId) {
|
||||||
|
clearTimeout(commentSubmitTimeoutId);
|
||||||
|
}
|
||||||
setCommentSubmitLoading(false);
|
setCommentSubmitLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -215,17 +342,45 @@ export default function CommentSectionWrapper(
|
|||||||
const createCommentHandler = async (
|
const createCommentHandler = async (
|
||||||
data: WebSocketBroadcast | BackupResponse
|
data: WebSocketBroadcast | BackupResponse
|
||||||
) => {
|
) => {
|
||||||
|
// Clear timeout since we received response
|
||||||
|
if (commentSubmitTimeoutId) {
|
||||||
|
clearTimeout(commentSubmitTimeoutId);
|
||||||
|
}
|
||||||
|
setOperationError("");
|
||||||
|
|
||||||
const body = data.commentBody;
|
const body = data.commentBody;
|
||||||
const commenterID = data.commenterID;
|
const commenterID = data.commenterID;
|
||||||
const parentCommentID = data.commentParent;
|
const parentCommentID = data.commentParent;
|
||||||
const id = data.commentID;
|
const id = data.commentID;
|
||||||
|
|
||||||
|
console.log("[createCommentHandler] Received data:", {
|
||||||
|
body,
|
||||||
|
commenterID,
|
||||||
|
parentCommentID,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
if (body && commenterID && parentCommentID !== undefined && id) {
|
if (body && commenterID && parentCommentID !== undefined && id) {
|
||||||
const domain = env.VITE_DOMAIN;
|
try {
|
||||||
const res = await fetch(
|
console.log(
|
||||||
`${domain}/api/database/user/public-data/${commenterID}`
|
"[createCommentHandler] Fetching user data for:",
|
||||||
|
commenterID
|
||||||
);
|
);
|
||||||
const userData = (await res.json()) as UserPublicData;
|
const userData = await api.database.getUserPublicData.query({
|
||||||
|
id: commenterID
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[createCommentHandler] User data response:", userData);
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch user data for commenter:",
|
||||||
|
commenterID,
|
||||||
|
"- Comment will not be displayed in UI but is saved in database"
|
||||||
|
);
|
||||||
|
setCommentSubmitLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const comment_date = getSQLFormattedDate();
|
const comment_date = getSQLFormattedDate();
|
||||||
const newComment: Comment = {
|
const newComment: Comment = {
|
||||||
@@ -246,7 +401,7 @@ export default function CommentSectionWrapper(
|
|||||||
}
|
}
|
||||||
setAllComments((prevComments) => [...(prevComments || []), newComment]);
|
setAllComments((prevComments) => [...(prevComments || []), newComment]);
|
||||||
|
|
||||||
const existingIDs = Array.from(userCommentMap.entries()).find(
|
const existingIDs = Array.from(userCommentMap().entries()).find(
|
||||||
([key, _]) =>
|
([key, _]) =>
|
||||||
key.email === userData.email &&
|
key.email === userData.email &&
|
||||||
key.display_name === userData.display_name &&
|
key.display_name === userData.display_name &&
|
||||||
@@ -255,9 +410,16 @@ export default function CommentSectionWrapper(
|
|||||||
|
|
||||||
if (existingIDs) {
|
if (existingIDs) {
|
||||||
const [key, ids] = existingIDs;
|
const [key, ids] = existingIDs;
|
||||||
userCommentMap.set(key, [...ids, id]);
|
const newMap = new Map(userCommentMap());
|
||||||
|
newMap.set(key, [...ids, id]);
|
||||||
|
setUserCommentMap(newMap);
|
||||||
} else {
|
} else {
|
||||||
userCommentMap.set(userData, [id]);
|
const newMap = new Map(userCommentMap());
|
||||||
|
newMap.set(userData, [id]);
|
||||||
|
setUserCommentMap(newMap);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user data:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCommentSubmitLoading(false);
|
setCommentSubmitLoading(false);
|
||||||
@@ -266,13 +428,26 @@ export default function CommentSectionWrapper(
|
|||||||
const editComment = async (body: string, comment_id: number) => {
|
const editComment = async (body: string, comment_id: number) => {
|
||||||
setCommentEditLoading(true);
|
setCommentEditLoading(true);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (editCommentTimeoutId) {
|
||||||
|
clearTimeout(editCommentTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout to clear loading state
|
||||||
|
editCommentTimeoutId = window.setTimeout(() => {
|
||||||
|
console.warn("Comment edit timed out");
|
||||||
|
setCommentEditLoading(false);
|
||||||
|
setOperationError("Comment edit timed out. Please try again.");
|
||||||
|
}, OPERATION_TIMEOUT);
|
||||||
|
|
||||||
if (!props.currentUserID) {
|
if (!props.currentUserID) {
|
||||||
console.warn("Cannot edit comment: user not authenticated");
|
console.warn("Cannot edit comment: user not authenticated");
|
||||||
|
clearTimeout(editCommentTimeoutId);
|
||||||
setCommentEditLoading(false);
|
setCommentEditLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socket) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -286,12 +461,25 @@ export default function CommentSectionWrapper(
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending comment update:", error);
|
console.error("Error sending comment update:", error);
|
||||||
|
setOperationError("Failed to edit comment. Please try again.");
|
||||||
|
clearTimeout(editCommentTimeoutId);
|
||||||
setCommentEditLoading(false);
|
setCommentEditLoading(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("WebSocket not available for edit, operation canceled");
|
||||||
|
setOperationError("Unable to edit comment. Please refresh the page.");
|
||||||
|
clearTimeout(editCommentTimeoutId);
|
||||||
|
setCommentEditLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const editCommentHandler = (data: WebSocketBroadcast) => {
|
const editCommentHandler = (data: WebSocketBroadcast) => {
|
||||||
|
// Clear timeout since we received response
|
||||||
|
if (editCommentTimeoutId) {
|
||||||
|
clearTimeout(editCommentTimeoutId);
|
||||||
|
}
|
||||||
|
setOperationError("");
|
||||||
|
|
||||||
setAllComments((prev) =>
|
setAllComments((prev) =>
|
||||||
prev.map((comment) => {
|
prev.map((comment) => {
|
||||||
if (comment.id === data.commentID) {
|
if (comment.id === data.commentID) {
|
||||||
@@ -339,10 +527,23 @@ export default function CommentSectionWrapper(
|
|||||||
|
|
||||||
setCommentDeletionLoading(true);
|
setCommentDeletionLoading(true);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (deleteCommentTimeoutId) {
|
||||||
|
clearTimeout(deleteCommentTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout to clear loading state
|
||||||
|
deleteCommentTimeoutId = window.setTimeout(() => {
|
||||||
|
console.warn("Comment deletion timed out");
|
||||||
|
setCommentDeletionLoading(false);
|
||||||
|
setOperationError("Comment deletion timed out. Please try again.");
|
||||||
|
}, OPERATION_TIMEOUT);
|
||||||
|
|
||||||
if (!props.currentUserID) {
|
if (!props.currentUserID) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[deleteComment] Cannot delete comment: user not authenticated"
|
"[deleteComment] Cannot delete comment: user not authenticated"
|
||||||
);
|
);
|
||||||
|
clearTimeout(deleteCommentTimeoutId);
|
||||||
setCommentDeletionLoading(false);
|
setCommentDeletionLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -365,12 +566,14 @@ export default function CommentSectionWrapper(
|
|||||||
"[deleteComment] WebSocket error, falling back to HTTP:",
|
"[deleteComment] WebSocket error, falling back to HTTP:",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
clearTimeout(deleteCommentTimeoutId);
|
||||||
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"[deleteComment] WebSocket not available, using HTTP fallback"
|
"[deleteComment] WebSocket not available, using HTTP fallback"
|
||||||
);
|
);
|
||||||
|
clearTimeout(deleteCommentTimeoutId);
|
||||||
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -404,11 +607,21 @@ export default function CommentSectionWrapper(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[fallbackCommentDeletion] Error:", error);
|
console.error("[fallbackCommentDeletion] Error:", error);
|
||||||
|
setOperationError("Failed to delete comment. Please try again.");
|
||||||
|
if (deleteCommentTimeoutId) {
|
||||||
|
clearTimeout(deleteCommentTimeoutId);
|
||||||
|
}
|
||||||
setCommentDeletionLoading(false);
|
setCommentDeletionLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCommentHandler = (data: WebSocketBroadcast) => {
|
const deleteCommentHandler = (data: WebSocketBroadcast) => {
|
||||||
|
// Clear timeout since we received response
|
||||||
|
if (deleteCommentTimeoutId) {
|
||||||
|
clearTimeout(deleteCommentTimeoutId);
|
||||||
|
}
|
||||||
|
setOperationError("");
|
||||||
|
|
||||||
if (data.commentBody) {
|
if (data.commentBody) {
|
||||||
// Soft delete (replace body with deletion message)
|
// Soft delete (replace body with deletion message)
|
||||||
setAllComments((prev) =>
|
setAllComments((prev) =>
|
||||||
@@ -620,6 +833,27 @@ export default function CommentSectionWrapper(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Connection status indicator (dev mode only) */}
|
||||||
|
<Show when={import.meta.env.DEV && connectionState() !== "connected"}>
|
||||||
|
<div class="mx-auto mb-2 w-3/4 text-center text-xs italic opacity-60">
|
||||||
|
<Show when={connectionState() === "connecting"}>
|
||||||
|
<span>⚡ Connecting to live updates...</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={connectionState() === "disconnected"}>
|
||||||
|
<span>📡 Live updates disconnected</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={connectionState() === "error"}>
|
||||||
|
<span class="text-red">❌ Connection error</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={operationError()}>
|
||||||
|
<div class="bg-red/20 border-red text-red mx-auto mb-4 w-3/4 rounded-lg border px-4 py-3 text-center">
|
||||||
|
{operationError()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<CommentSection
|
<CommentSection
|
||||||
privilegeLevel={props.privilegeLevel}
|
privilegeLevel={props.privilegeLevel}
|
||||||
allComments={allComments()}
|
allComments={allComments()}
|
||||||
@@ -627,7 +861,7 @@ export default function CommentSectionWrapper(
|
|||||||
postID={props.id}
|
postID={props.id}
|
||||||
reactionMap={currentReactionMap()}
|
reactionMap={currentReactionMap()}
|
||||||
currentUserID={props.currentUserID}
|
currentUserID={props.currentUserID}
|
||||||
userCommentMap={userCommentMap}
|
userCommentMap={userCommentMap()}
|
||||||
newComment={newComment}
|
newComment={newComment}
|
||||||
commentSubmitLoading={commentSubmitLoading()}
|
commentSubmitLoading={commentSubmitLoading()}
|
||||||
toggleModification={toggleModification}
|
toggleModification={toggleModification}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function EditCommentModal(props: EditCommentModalProps) {
|
|||||||
open={props.isOpen}
|
open={props.isOpen}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
title="Edit Comment"
|
title="Edit Comment"
|
||||||
class="bg-surface1 w-11/12 max-w-none sm:w-4/5 md:w-2/3"
|
class="bg-crust w-11/12 max-w-none sm:w-4/5 md:w-2/3"
|
||||||
>
|
>
|
||||||
<form onSubmit={editCommentWrapper}>
|
<form onSubmit={editCommentWrapper}>
|
||||||
<div class="textarea-group home">
|
<div class="textarea-group home">
|
||||||
|
|||||||
@@ -31,20 +31,22 @@ export default function Modal(props: ModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (props.open) {
|
if (props.open && typeof document !== "undefined") {
|
||||||
document.addEventListener("keydown", handleEscapeKey);
|
document.addEventListener("keydown", handleEscapeKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
document.removeEventListener("keydown", handleEscapeKey);
|
document.removeEventListener("keydown", handleEscapeKey);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<Show when={props.open}>
|
||||||
<Portal>
|
<Portal>
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
class="fixed inset-0 z-500 flex items-center justify-center bg-black/50"
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
|||||||
Reference in New Issue
Block a user