import { useMemo, useState, useCallback } from "react"; import { View, ScrollView, TextInput, Pressable, Text } from "react-native"; import { Avatar, Button, Menu } from "react-native-paper"; import { Ionicons } from "@expo/vector-icons"; import { formatDistanceToNow, formatDistanceToNowStrict } from "date-fns"; import { useGetTodoCommentsQuery, useAddCommentMutation, useUpdateCommentMutation, useDeleteCommentMutation, } from "@/store/api/commentsApi"; import { useAuthContext } from "@/auth/AuthContext"; import { useGetUsersQuery } from "@/store/api/usersApi"; import { AppSnackbar } from "@/components/ui/AppSnackbar"; interface TodoCommentsProps { todoId: string; } // Helper: compact relative time like "2h ago" const timeAgo = (dateInput: string | Date | null | undefined) => { if (!dateInput) return ""; const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput; try { const raw = formatDistanceToNowStrict(date, { addSuffix: false }); // raw examples: "2 hours", "1 minute", "less than a minute" const m = raw.match( /(\d+)\s+(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years)/ ); if (m) { const n = m[1]; const unit = m[2]; const abbr = unit.startsWith("second") ? "s" : unit.startsWith("minute") ? "m" : unit.startsWith("hour") ? "h" : unit.startsWith("day") ? "d" : unit.startsWith("week") ? "w" : unit.startsWith("month") ? "mo" : "y"; return `${n}${abbr} ago`; } // fallback to full format return formatDistanceToNow(date, { addSuffix: true }); } catch { return ""; } }; const LONG_COMMENT_THRESHOLD = 220; interface TodoCommentsProps { todoId: string; } export const TodoComments = ({ todoId }: TodoCommentsProps) => { const { user } = useAuthContext(); const { data: comments, isLoading, isError, refetch, } = useGetTodoCommentsQuery(todoId); const [addComment, { isLoading: isAdding }] = useAddCommentMutation(); const [updateComment, { isLoading: isUpdating }] = useUpdateCommentMutation(); const [deleteComment, { isLoading: isDeleting }] = useDeleteCommentMutation(); const [content, setContent] = useState(""); const [editingId, setEditingId] = useState(null); const [editContent, setEditContent] = useState(""); const [snackbarMessage, setSnackbarMessage] = useState(""); const [snackbarVisible, setSnackbarVisible] = useState(false); const [expanded, setExpanded] = useState>({}); const [menuId, setMenuId] = useState(null); const showSnackbar = (message: string) => { setSnackbarMessage(message); setSnackbarVisible(true); }; const canPost = useMemo(() => !!user, [user]); const handleSubmit = async () => { const trimmed = content.trim(); if (!trimmed) return; try { await addComment({ todoId, content: trimmed }).unwrap(); setContent(""); } catch { showSnackbar("Error creating comment"); } }; const startEdit = (id: string, current: string) => { setEditingId(id); setEditContent(current); }; const cancelEdit = () => { setEditingId(null); setEditContent(""); }; const saveEdit = async () => { const trimmed = editContent.trim(); if (!editingId || !trimmed) return; try { await updateComment({ todoId, commentId: editingId, content: trimmed, }).unwrap(); setEditingId(null); setEditContent(""); } catch { showSnackbar("Error updating comment"); } }; const onDelete = async (id: string) => { try { // simple confirm for web; on native it won't show but action still works if called if (typeof window !== "undefined") { const ok = window.confirm?.("Delete this comment?") ?? true; if (!ok) return; } await deleteComment({ todoId, commentId: id }).unwrap(); } catch { showSnackbar("Error deleting comment"); } }; // Mention support const extractMentionQuery = useCallback((text: string) => { // find last segment starting with @ that has no whitespace afterwards yet const match = text.match(/@([\w.-]{1,30})$/); return match ? match[1] : ""; }, []); const mentionQuery = extractMentionQuery(editingId ? editContent : content); const { data: mentionUsers } = useGetUsersQuery( mentionQuery ? { search: mentionQuery, page: 1, limit: 5, roleType: "admin" } : undefined, { skip: !mentionQuery } ); const insertMention = (fullName: string) => { const setter = editingId ? setEditContent : setContent; const text = editingId ? editContent : content; // replace trailing @query with @Full Name and a space const replaced = text.replace(/@([\w.-]{1,30})$/, `@${fullName} `); setter(replaced); }; const renderContentWithMentions = (text: string) => { const parts = text.split(/(@[\w.-]+)/g); return parts.map((p, i) => p.startsWith("@") ? ( {p} ) : ( {p} ) ); }; const getCommentAuthorName = (commentUser: { id: string; firstName?: string; lastName?: string; }) => { if (!commentUser) return "Unknown User"; if (commentUser.id === user?.id) return "You"; return `${(commentUser.firstName || "").trim()} ${( commentUser.lastName || "" ).trim()}`.trim(); }; return ( <> Comments {isLoading ? ( Loading comments... ) : isError ? ( Failed to load comments ) : comments && comments.length > 0 ? ( {comments.map((c, idx) => { const isExpanded = !!expanded[c.id]; const isLong = (c.content?.length || 0) > LONG_COMMENT_THRESHOLD; const prevUserId = idx > 0 ? comments[idx - 1]?.user?.id : null; const isNewUserBlock = idx > 0 && prevUserId !== c.user?.id; return ( {isNewUserBlock ? ( ) : null} {c.user?.imageUrl ? ( ) : ( )} {getCommentAuthorName(c.user)} {timeAgo(c.createdAt)} {user?.id === c.user?.id && editingId !== c.id ? ( setMenuId(null)} anchor={ setMenuId(c.id)} style={{ height: 18, width: 18, alignItems: "center", justifyContent: "center", }} > } > { setMenuId(null); startEdit(c.id, c.content); }} title="Edit" /> { setMenuId(null); onDelete(c.id); }} disabled={isDeleting} title="Delete" /> ) : null} {editingId === c.id ? ( {mentionQuery && mentionUsers?.items?.length ? ( {mentionUsers.items.map((u) => ( insertMention( `${u.firstName} ${u.lastName}` ) } > @{u.firstName} {u.lastName} ))} ) : null} ) : ( {renderContentWithMentions(c.content)} {isLong ? ( setExpanded((prev) => ({ ...prev, [c.id]: !isExpanded, })) } > {isExpanded ? "Show less" : "Show more"} ) : null} )} ); })} ) : ( No comments yet )} {canPost ? ( {mentionQuery && mentionUsers?.items?.length ? ( {mentionUsers.items.map((u) => ( insertMention(`${u.firstName} ${u.lastName}`) } > @{u.firstName} {u.lastName} ))} ) : null} ) : null} setSnackbarVisible(false)} action={{ label: "Close", onPress: () => setSnackbarVisible(false) }} > {snackbarMessage} ); };