Repository: copilot-platforms/tasks-app
Pull request #671: OUT-1487 | Add support for optimistic update and animations for replies while creating/deleting
Author: login: arpandhakal
Created: 2025-03-06T12:36:16Z | Merged: 2025-03-07T12:42:26Z

## Changes

- [x] added optimistic updates for replies on creation.
- [x] added collapse animations on replies. 
- [x] added optimistic updates for replies on deletion.
- [x] shared functions like `getStableId`, creation, deletion of comments to replies on collapsed mode. 

## Testing Criteria

- [LOOM](https://www.loom.com/share/b8695656c4284949a9fa379c7b0c2eaf?focus_title=1&muted=1&from_recorder=1)

## Impact & Surface Area of Change

- general comment/activity-log functionality because replies share all the endpoints and functions related to comment/activity-logs. 








Diff:
diff --git a/src/app/detail/ui/ActivityWrapper.tsx b/src/app/detail/ui/ActivityWrapper.tsx
index ec7aa8924..581d05645 100644
--- a/src/app/detail/ui/ActivityWrapper.tsx
+++ b/src/app/detail/ui/ActivityWrapper.tsx
@@ -19,6 +19,8 @@ import useSWR, { useSWRConfig } from 'swr'
 import { z } from 'zod'
 import { TransitionGroup } from 'react-transition-group'
 import { selectTaskDetails } from '@/redux/features/taskDetailsSlice'
+import { ReplyResponse } from '@/app/api/activity-logs/schemas/CommentAddedSchema'
+import { checkOptimisticStableId } from '@/utils/checkOptimisticStableId'
 
 interface OptimisticUpdate {
   tempId: string
@@ -68,17 +70,6 @@ export const ActivityWrapper = ({
     return z.union([InternalUsersSchema, ClientResponseSchema]).parse(assignee.find((el) => el.id === currentUserId))
   }, [assignee, currentUserId])
 
-  const getStableId = (log: LogResponse) => {
-    if (log.type === ActivityType.COMMENT_ADDED) {
-      const matchingUpdate = optimisticUpdates.find(
-        (update) => update.tempId === log.id || (log.details.id && update.serverId === log.details.id),
-      )
-      if (matchingUpdate) {
-        return matchingUpdate?.tempId
-      }
-    }
-    return log.id
-  }
   // Handle comment creation
   const handleCreateComment = async (postCommentPayload: CreateComment) => {
     const tempId = generateRandomString('temp-comment')
@@ -91,35 +82,86 @@ export const ActivityWrapper = ({
       },
     ])
 
-    const tempLog: LogResponse = {
-      id: tempId,
-      type: ActivityType.COMMENT_ADDED,
-      details: {
-        content: postCommentPayload.content,
-        id: tempId,
-      },
-      taskId: task_id,
-      userId: currentUserId as string,
-      userRole: 'internalUser',
-      workspaceId: '',
-      initiator: {
-        status: '',
-        id: currentUserDetails?.id as string,
-        givenName: currentUserDetails?.givenName || '',
-        familyName: currentUserDetails?.familyName || '',
-        email: currentUserDetails?.email || '',
-        companyId: '',
-        avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
-        customFields: {},
-        fallbackColor: currentUserDetails?.fallbackColor || null,
-        createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
-        isClientAccessLimited: false,
-        companyAccessList: null,
-      },
-      createdAt: new Date().toISOString(),
+    const tempLog: LogResponse | ReplyResponse = postCommentPayload.parentId
+      ? {
+          id: tempId,
+          content: postCommentPayload.content,
+          taskId: task_id,
+          parentId: postCommentPayload.parentId,
+          workspaceId: '',
+          initiatorId: currentUserDetails?.id as string,
+          initiator: {
+            status: '',
+            id: currentUserDetails?.id as string,
+            givenName: currentUserDetails?.givenName || '',
+            familyName: currentUserDetails?.familyName || '',
+            email: currentUserDetails?.email || '',
+            companyId: '',
+            avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
+            customFields: {},
+            fallbackColor: currentUserDetails?.fallbackColor || null,
+            createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
+            isClientAccessLimited: false,
+            companyAccessList: null,
+          },
+          createdAt: new Date().toISOString(),
+          updatedAt: new Date().toISOString(),
+        }
+      : {
+          id: tempId,
+          type: ActivityType.COMMENT_ADDED,
+          details: {
+            content: postCommentPayload.content,
+            id: tempId,
+            replies: [],
+          },
+          taskId: task_id,
+          userId: currentUserId as string,
+          userRole: 'internalUser',
+          workspaceId: '',
+          initiator: {
+            status: '',
+            id: currentUserDetails?.id as string,
+            givenName: currentUserDetails?.givenName || '',
+            familyName: currentUserDetails?.familyName || '',
+            email: currentUserDetails?.email || '',
+            companyId: '',
+            avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
+            customFields: {},
+            fallbackColor: currentUserDetails?.fallbackColor || null,
+            createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
+            isClientAccessLimited: false,
+            companyAccessList: null,
+          },
+          createdAt: new Date().toISOString(),
+        }
+
+    let optimisticData
+    if (postCommentPayload.parentId) {
+      optimisticData = activities
+        ? activities.data.map((comment: LogResponse) => {
+            if (comment.details.id === postCommentPayload.parentId) {
+              const updatedReplies = [
+                ...(comment.details.replies as ReplyResponse[]),
+                { ...tempLog, parentId: postCommentPayload.parentId },
+              ]
+
+              return {
+                ...comment,
+                replies: updatedReplies,
+                details: {
+                  ...comment.details,
+                  replies: updatedReplies,
+                },
+              }
+            }
+            return comment
+          })
+        : []
+    } else {
+      optimisticData = activities ? [...activities.data, tempLog] : [tempLog]
     }
 
-    const optimisticData = activities ? [...activities.data, tempLog] : [tempLog]
     try {
       mutate(
         cacheKey,
@@ -133,7 +175,7 @@ export const ActivityWrapper = ({
           return await fetcher(cacheKey)
         },
         {
-          optimisticData: postCommentPayload.parentId ? undefined : { data: optimisticData },
+          optimisticData: { data: optimisticData },
           rollbackOnError: true,
           revalidate: true, // Make sure to revalidate after mutation
         },
@@ -145,8 +187,29 @@ export const ActivityWrapper = ({
   }
 
   // Handle comment deletion
-  const handleDeleteComment = async (commentId: string, logId: string) => {
-    const optimisticData = activities ? activities.data.filter((comment: LogResponse) => comment.id !== logId) : []
+  const handleDeleteComment = async (commentId: string, logId: string, replyId?: string) => {
+    let optimisticData
+    if (replyId) {
+      optimisticData = activities
+        ? activities.data.map((item: LogResponse) => {
+            if (item.id === logId) {
+              const updatedReplies = (item.details.replies as ReplyResponse[]).filter(
+                (reply: ReplyResponse) => reply.id !== replyId,
+              )
+              return {
+                ...item,
+                details: {
+                  ...item.details,
+                  replies: updatedReplies,
+                },
+              }
+            }
+            return item
+          })
+        : []
+    } else {
+      optimisticData = activities ? activities.data.filter((comment: LogResponse) => comment.id !== logId) : []
+    }
 
     try {
       await mutate(
@@ -182,7 +245,7 @@ export const ActivityWrapper = ({
           <Stack direction="column" alignItems="left" rowGap={2}>
             <TransitionGroup>
               {activities?.data?.map((item: LogResponse, index: number) => (
-                <Collapse key={getStableId(item)}>
+                <Collapse key={checkOptimisticStableId(item, optimisticUpdates)}>
                   <Box
                     key={index}
                     sx={{
@@ -193,9 +256,10 @@ export const ActivityWrapper = ({
                       <Comments
                         comment={item}
                         createComment={handleCreateComment}
-                        deleteComment={(commentId) => handleDeleteComment(commentId, item.id)}
+                        deleteComment={(commentId, replyId) => handleDeleteComment(commentId, item.id, replyId)}
                         task_id={task_id}
                         stableId={item.id}
+                        optimisticUpdates={optimisticUpdates}
                       />
                     ) : (
                       <ActivityLog log={item} />
diff --git a/src/app/detail/ui/Comments.tsx b/src/app/detail/ui/Comments.tsx
index 3771ec363..638403846 100644
--- a/src/app/detail/ui/Comments.tsx
+++ b/src/app/detail/ui/Comments.tsx
@@ -5,16 +5,18 @@ import { IAssigneeCombined } from '@/types/interfaces'
 import { LogResponse } from '@api/activity-logs/schemas/LogResponseSchema'
 import { Stack } from '@mui/material'
 import { VerticalLine } from './styledComponent'
+import { OptimisticUpdate } from '@/utils/checkOptimisticStableId'
 
 interface Prop {
   comment: LogResponse
   createComment: (postCommentPayload: CreateComment) => void
-  deleteComment: (id: string) => void
+  deleteComment: (id: string, replyId?: string) => void
   task_id: string
   stableId: string
+  optimisticUpdates: OptimisticUpdate[]
 }
 
-export const Comments = ({ comment, createComment, deleteComment, task_id, stableId }: Prop) => {
+export const Comments = ({ comment, createComment, deleteComment, task_id, stableId, optimisticUpdates }: Prop) => {
   return (
     <Stack id={stableId} direction="row" columnGap={2} position="relative">
       <VerticalLine />
@@ -31,7 +33,13 @@ export const Comments = ({ comment, createComment, deleteComment, task_id, stabl
           size="large"
         />
 
-        <CommentCard comment={comment} createComment={createComment} deleteComment={deleteComment} task_id={task_id} />
+        <CommentCard
+          comment={comment}
+          createComment={createComment}
+          deleteComment={deleteComment}
+          task_id={task_id}
+          optimisticUpdates={optimisticUpdates}
+        />
       </Stack>
     </Stack>
   )
diff --git a/src/components/cards/CommentCard.tsx b/src/components/cards/CommentCard.tsx
index c2690c94c..d799fdd31 100644
--- a/src/components/cards/CommentCard.tsx
+++ b/src/components/cards/CommentCard.tsx
@@ -1,6 +1,6 @@
 'use client'
 
-import { Box, Stack, Typography } from '@mui/material'
+import { Box, Collapse, Stack, Typography } from '@mui/material'
 import { useEffect, useMemo, useRef, useState } from 'react'
 import { useSelector } from 'react-redux'
 import { Tapwrite } from 'tapwrite'
@@ -43,21 +43,24 @@ import { ReplyInput } from '@/components/inputs/ReplyInput'
 import useSWR from 'swr'
 import { fetcher } from '@/utils/fetcher'
 import { CollapsibleReplyCard } from '@/components/cards/CollapsibleReplyCard'
+import { TransitionGroup } from 'react-transition-group'
+import { checkOptimisticStableId, OptimisticUpdate } from '@/utils/checkOptimisticStableId'
 
 export const CommentCard = ({
   comment,
   createComment,
   deleteComment,
   task_id,
+  optimisticUpdates,
 }: {
   comment: LogResponse
   createComment: (postCommentPayload: CreateComment) => void
-  deleteComment: (id: string) => void
+  deleteComment: (id: string, replyId?: string) => void
   task_id: string
+  optimisticUpdates: OptimisticUpdate[]
 }) => {
   const [showReply, setShowReply] = useState(false)
   const [isHovered, setIsHovered] = useState(false)
-
   const [replies, setReplies] = useState(comment.details.replies as ReplyResponse[])
 
   const [timeAgo, setTimeAgo] = useState(getTimeDifference(comment.createdAt))
@@ -154,26 +157,33 @@ export const CommentCard = ({
     refreshWhenOffline: false,
     refreshWhenHidden: false,
     refreshInterval: 0,
+    optimisticData: optimisticUpdates.filter((update) => update.tempId),
   })
   const fetchCommentsWithFullReplies = async () => {
     try {
       const updatedComment = await mutate()
 
       setReplies(updatedComment?.comments || comment.details.replies || [])
-      store.dispatch(setExpandedComments([...expandedComments, z.string().parse(comment.details.id)]))
+      store.dispatch(setExpandedComments([...expandedComments, z.string().parse(comment.details.id ?? '')]))
     } catch (error) {
       console.error('Failed to fetch replies:', error)
     }
   }
 
   useEffect(() => {
-    if (expandedComments.includes(z.string().parse(comment.details.id))) {
-      fetchCommentsWithFullReplies()
+    const replies = (comment.details.replies as ReplyResponse[]) || []
+    if (expandedComments.length && expandedComments.includes(z.string().parse(comment.details.id))) {
+      const lastReply = replies[replies.length - 1]
+      if (lastReply && lastReply.id.includes('temp-comment')) {
+        setReplies((prev) => [...prev, lastReply])
+        return
+      } else {
+        fetchCommentsWithFullReplies()
+      }
     } else {
-      setReplies((comment.details.replies as ReplyResponse[]) || [])
+      setReplies(replies)
     }
   }, [comment])
-
   return (
     <CommentCardContainer
       sx={{
@@ -205,48 +215,51 @@ export const CommentCard = ({
                 </StyledTypography>
               </Stack>
 
-              {(isHovered || isMobile() || isMenuOpen) && canEdit && (
+              {(isHovered || isMobile() || isMenuOpen) && (
                 <Stack direction="row" columnGap={2} sx={{ height: '10px' }} alignItems="center">
                   <ReplyButton handleClick={() => setShowReply((prev) => !prev)} />
-                  <MenuBox
-                    getMenuOpen={(open) => {
-                      setIsMenuOpen(open)
-                    }}
-                    menuContent={
-                      <>
-                        <ListBtn
-                          content="Edit comment"
-                          handleClick={() => {
-                            setIsReadOnly(false)
-                            if (editRef.current) {
-                              editRef.current.focus()
-                            }
-                          }}
-                          icon={<PencilIcon />}
-                          contentColor={(theme) => theme.color.text.text}
-                          width="175px"
-                          height="33px"
-                        />
-                        {canDelete && (
+                  {canEdit && (
+                    <MenuBox
+                      getMenuOpen={(open) => {
+                        setIsMenuOpen(open)
+                      }}
+                      menuContent={
+                        <>
                           <ListBtn
-                            content="Delete comment"
+                            content="Edit comment"
                             handleClick={() => {
-                              setShowConfirmDeleteModal(true)
+                              setIsReadOnly(false)
+                              if (editRef.current) {
+                                editRef.current.focus()
+                              }
                             }}
-                            icon={<TrashIcon />}
-                            contentColor={(theme) => theme.color.error}
+                            icon={<PencilIcon />}
+                            contentColor={(theme) => theme.color.text.text}
                             width="175px"
                             height="33px"
                           />
-                        )}
-                      </>
-                    }
-                    isSecondary
-                    width={'22px'}
-                    height={'22px'}
-                    displayButtonBackground={false}
-                    noHover={false}
-                  />
+
+                          {canDelete && (
+                            <ListBtn
+                              content="Delete comment"
+                              handleClick={() => {
+                                setShowConfirmDeleteModal(true)
+                              }}
+                              icon={<TrashIcon />}
+                              contentColor={(theme) => theme.color.error}
+                              width="175px"
+                              height="33px"
+                            />
+                          )}
+                        </>
+                      }
+                      isSecondary
+                      width={'22px'}
+                      height={'22px'}
+                      displayButtonBackground={false}
+                      noHover={false}
+                    />
+                  )}
                 </Stack>
               )}
             </Stack>
@@ -282,11 +295,9 @@ export const CommentCard = ({
             />
           </Box>
         </Stack>
-
         {((Array.isArray((comment as LogResponse).details?.replies) &&
           ((comment as LogResponse).details.replies as ReplyResponse[]).length > 0) ||
           showReply) && <CustomDivider />}
-
         {replyCount > 3 && !expandedComments.includes(z.string().parse(comment.details.id)) && (
           <CollapsibleReplyCard
             lastAssignees={comment.details.firstInitiators as IAssigneeCombined[]}
@@ -294,20 +305,22 @@ export const CommentCard = ({
             replyCount={replyCount}
           />
         )}
-
-        {Array.isArray((comment as LogResponse).details?.replies) &&
-          replies.map((item: ReplyResponse) => {
-            return (
-              <ReplyCard
-                item={item}
-                key={item.id}
-                uploadFn={uploadFn}
-                task_id={task_id}
-                handleImagePreview={handleImagePreview}
-              />
-            )
-          })}
-
+        <TransitionGroup>
+          {Array.isArray((comment as LogResponse).details?.replies) &&
+            replies.map((item: ReplyResponse) => {
+              return (
+                <Collapse key={checkOptimisticStableId(item, optimisticUpdates)}>
+                  <ReplyCard
+                    item={item}
+                    uploadFn={uploadFn}
+                    task_id={task_id}
+                    handleImagePreview={handleImagePreview}
+                    deleteReply={deleteComment}
+                  />
+                </Collapse>
+              )
+            })}
+        </TransitionGroup>
         {(Array.isArray((comment as LogResponse).details?.replies) &&
           ((comment as LogResponse).details.replies as LogResponse[]).length > 0) ||
         showReply ? (
diff --git a/src/components/cards/ReplyCard.tsx b/src/components/cards/ReplyCard.tsx
index cad25f7d8..e9aaac32a 100644
--- a/src/components/cards/ReplyCard.tsx
+++ b/src/components/cards/ReplyCard.tsx
@@ -8,7 +8,6 @@ import { BoldTypography, CustomDivider, StyledModal, StyledTypography } from '@/
 import { CopilotAvatar } from '@/components/atoms/CopilotAvatar'
 import AttachmentLayout from '@/components/AttachmentLayout'
 import { ListBtn } from '@/components/buttons/ListBtn'
-import { PrimaryBtn } from '@/components/buttons/PrimaryBtn'
 import { EditCommentButtons } from '@/components/buttonsGroup/EditCommentButtons'
 import { MenuBox } from '@/components/inputs/MenuBox'
 import { ConfirmDeleteUI } from '@/components/layouts/ConfirmDeleteUI'
@@ -33,11 +32,13 @@ export const ReplyCard = ({
   uploadFn,
   task_id,
   handleImagePreview,
+  deleteReply,
 }: {
   item: ReplyResponse
   uploadFn: ((file: File) => Promise<string | undefined>) | undefined
   task_id: string
   handleImagePreview: (e: React.MouseEvent<unknown>) => void
+  deleteReply: (id: string, replyId: string) => void
 }) => {
   const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
   const [isMenuOpen, setIsMenuOpen] = useState(false)
@@ -233,10 +234,11 @@ export const ReplyCard = ({
           <ConfirmDeleteUI
             handleCancel={() => setShowConfirmDeleteModal(false)}
             handleDelete={() => {
-              deleteComment(token ?? '', item.id as string)
+              deleteReply(item.id as string, item.id)
               setShowConfirmDeleteModal(false)
             }}
-            bodyTag="comment"
+            bodyTag="message"
+            description="Are you sure you want to delete this message?"
           />
         </StyledModal>
       </Stack>
diff --git a/src/components/inputs/ReplyInput.tsx b/src/components/inputs/ReplyInput.tsx
index 6c8505833..bea5023a4 100644
--- a/src/components/inputs/ReplyInput.tsx
+++ b/src/components/inputs/ReplyInput.tsx
@@ -52,7 +52,7 @@ export const ReplyInput = ({ task_id, comment, createComment, uploadFn }: ReplyI
   }, [comment, detail, task_id])
 
   useEffect(() => {
-    if (pendingReplies.length > 0 && comment.details.id) {
+    if (pendingReplies.length > 0 && !comment.details.id.includes('temp-comment')) {
       const { content, taskId } = pendingReplies[0]
       createComment({
         content,
diff --git a/src/components/layouts/ConfirmDeleteUI.tsx b/src/components/layouts/ConfirmDeleteUI.tsx
index b46ffa62c..00696dd42 100644
--- a/src/components/layouts/ConfirmDeleteUI.tsx
+++ b/src/components/layouts/ConfirmDeleteUI.tsx
@@ -9,7 +9,7 @@ import { PrimaryBtn } from '../buttons/PrimaryBtn'
 interface Prop {
   handleCancel: () => void
   handleDelete: () => void
-  bodyTag?: 'task' | 'comment' | 'template'
+  bodyTag?: 'task' | 'comment' | 'template' | 'message'
   customBody?: string
   description?: string
 }
@@ -24,6 +24,7 @@ export const ConfirmDeleteUI = ({
   const getHeaderMessage = () => {
     if (customBody) return customBody
     if (bodyTag === 'comment') return 'Delete comment?'
+    if (bodyTag === 'message') return 'Delete message'
     return `Are you sure you want to delete this ${bodyTag}?`
   }
 
diff --git a/src/utils/checkOptimisticStableId.ts b/src/utils/checkOptimisticStableId.ts
new file mode 100644
index 000000000..318e7a502
--- /dev/null
+++ b/src/utils/checkOptimisticStableId.ts
@@ -0,0 +1,16 @@
+import { ReplyResponse } from '@/app/api/activity-logs/schemas/CommentAddedSchema'
+import { LogResponse } from '@/app/api/activity-logs/schemas/LogResponseSchema'
+import { ActivityType } from '@prisma/client'
+
+export interface OptimisticUpdate {
+  tempId: string
+  serverId?: string
+  timestamp: number
+}
+
+//util to maintain same key for collapse animation on optimistic updates
+export const checkOptimisticStableId = (log: LogResponse | ReplyResponse, optimisticUpdates: OptimisticUpdate[]) => {
+  const referenceId = 'details' in log ? (log.details.id ?? log.id) : log.id
+  const matchingUpdate = optimisticUpdates.find((update) => update.tempId === log.id || update.serverId === referenceId)
+  return matchingUpdate ? matchingUpdate.tempId : log.id
+}


Reviews:
user: login: rrojan
body: Approved for now. Let's deal with these in a follow up ticket
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:42:14Z
user: login: arpandhakal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T08:33:08Z

Review comments:
diff_hunk: @@ -24,6 +24,7 @@ export const ConfirmDeleteUI = ({
   const getHeaderMessage = () => {
     if (customBody) return customBody
     if (bodyTag === 'comment') return 'Delete comment?'
+    if (bodyTag === 'message') return 'Delete message'
path: src/components/layouts/ConfirmDeleteUI.tsx
user: login: rrojan
body: ```suggestion
    if (bodyTag === 'message') return 'Delete message?'
```
created_at: 2025-03-07T12:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+import { ReplyResponse } from '@/app/api/activity-logs/schemas/CommentAddedSchema'
+import { LogResponse } from '@/app/api/activity-logs/schemas/LogResponseSchema'
+import { ActivityType } from '@prisma/client'
+
+export interface OptimisticUpdate {
+  tempId: string
+  serverId?: string
+  timestamp: number
+}
+
+//util to maintain same key for collapse animation on optimistic updates
+export const checkOptimisticStableId = (log: LogResponse | ReplyResponse, optimisticUpdates: OptimisticUpdate[]) => {
+  const referenceId = 'details' in log ? (log.details.id ?? log.id) : log.id
+  const matchingUpdate = optimisticUpdates.find((update) => update.tempId === log.id || update.serverId === referenceId)
path: src/utils/checkOptimisticStableId.ts
user: login: rrojan
body: This is a bit hard to understand. Is it possible to refactor this, or add comments
created_at: 2025-03-07T12:38:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,8 +187,29 @@ export const ActivityWrapper = ({
   }
 
   // Handle comment deletion
-  const handleDeleteComment = async (commentId: string, logId: string) => {
-    const optimisticData = activities ? activities.data.filter((comment: LogResponse) => comment.id !== logId) : []
+  const handleDeleteComment = async (commentId: string, logId: string, replyId?: string) => {
+    let optimisticData
+    if (replyId) {
+      optimisticData = activities
+        ? activities.data.map((item: LogResponse) => {
+            if (item.id === logId) {
+              const updatedReplies = (item.details.replies as ReplyResponse[]).filter(
+                (reply: ReplyResponse) => reply.id !== replyId,
+              )
+              return {
+                ...item,
+                details: {
+                  ...item.details,
+                  replies: updatedReplies,
+                },
+              }
+            }
+            return item
+          })
+        : []
+    } else {
path: src/app/detail/ui/ActivityWrapper.tsx
user: login: rrojan
body: We can skip else here if all conditions before this return a value
created_at: 2025-03-07T12:40:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,35 +82,86 @@ export const ActivityWrapper = ({
       },
     ])
 
-    const tempLog: LogResponse = {
-      id: tempId,
-      type: ActivityType.COMMENT_ADDED,
-      details: {
-        content: postCommentPayload.content,
-        id: tempId,
-      },
-      taskId: task_id,
-      userId: currentUserId as string,
-      userRole: 'internalUser',
-      workspaceId: '',
-      initiator: {
-        status: '',
-        id: currentUserDetails?.id as string,
-        givenName: currentUserDetails?.givenName || '',
-        familyName: currentUserDetails?.familyName || '',
-        email: currentUserDetails?.email || '',
-        companyId: '',
-        avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
-        customFields: {},
-        fallbackColor: currentUserDetails?.fallbackColor || null,
-        createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
-        isClientAccessLimited: false,
-        companyAccessList: null,
-      },
-      createdAt: new Date().toISOString(),
+    const tempLog: LogResponse | ReplyResponse = postCommentPayload.parentId
+      ? {
+          id: tempId,
+          content: postCommentPayload.content,
+          taskId: task_id,
+          parentId: postCommentPayload.parentId,
+          workspaceId: '',
+          initiatorId: currentUserDetails?.id as string,
+          initiator: {
+            status: '',
+            id: currentUserDetails?.id as string,
+            givenName: currentUserDetails?.givenName || '',
+            familyName: currentUserDetails?.familyName || '',
+            email: currentUserDetails?.email || '',
+            companyId: '',
+            avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
+            customFields: {},
+            fallbackColor: currentUserDetails?.fallbackColor || null,
+            createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
+            isClientAccessLimited: false,
+            companyAccessList: null,
+          },
+          createdAt: new Date().toISOString(),
+          updatedAt: new Date().toISOString(),
+        }
+      : {
+          id: tempId,
+          type: ActivityType.COMMENT_ADDED,
+          details: {
+            content: postCommentPayload.content,
path: src/app/detail/ui/ActivityWrapper.tsx
user: login: rrojan
body: Please extract this into a separate comment 
created_at: 2025-03-07T12:41:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,35 +82,86 @@ export const ActivityWrapper = ({
       },
     ])
 
-    const tempLog: LogResponse = {
-      id: tempId,
-      type: ActivityType.COMMENT_ADDED,
-      details: {
-        content: postCommentPayload.content,
-        id: tempId,
-      },
-      taskId: task_id,
-      userId: currentUserId as string,
-      userRole: 'internalUser',
-      workspaceId: '',
-      initiator: {
-        status: '',
-        id: currentUserDetails?.id as string,
-        givenName: currentUserDetails?.givenName || '',
-        familyName: currentUserDetails?.familyName || '',
-        email: currentUserDetails?.email || '',
-        companyId: '',
-        avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
-        customFields: {},
-        fallbackColor: currentUserDetails?.fallbackColor || null,
-        createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
-        isClientAccessLimited: false,
-        companyAccessList: null,
-      },
-      createdAt: new Date().toISOString(),
+    const tempLog: LogResponse | ReplyResponse = postCommentPayload.parentId
+      ? {
+          id: tempId,
+          content: postCommentPayload.content,
+          taskId: task_id,
+          parentId: postCommentPayload.parentId,
+          workspaceId: '',
+          initiatorId: currentUserDetails?.id as string,
+          initiator: {
+            status: '',
+            id: currentUserDetails?.id as string,
+            givenName: currentUserDetails?.givenName || '',
+            familyName: currentUserDetails?.familyName || '',
+            email: currentUserDetails?.email || '',
+            companyId: '',
+            avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
+            customFields: {},
+            fallbackColor: currentUserDetails?.fallbackColor || null,
+            createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
+            isClientAccessLimited: false,
+            companyAccessList: null,
+          },
+          createdAt: new Date().toISOString(),
+          updatedAt: new Date().toISOString(),
+        }
+      : {
+          id: tempId,
+          type: ActivityType.COMMENT_ADDED,
+          details: {
+            content: postCommentPayload.content,
+            id: tempId,
+            replies: [],
+          },
+          taskId: task_id,
+          userId: currentUserId as string,
+          userRole: 'internalUser',
+          workspaceId: '',
+          initiator: {
+            status: '',
+            id: currentUserDetails?.id as string,
+            givenName: currentUserDetails?.givenName || '',
+            familyName: currentUserDetails?.familyName || '',
+            email: currentUserDetails?.email || '',
+            companyId: '',
+            avatarImageUrl: currentUserDetails?.avatarImageUrl || null,
+            customFields: {},
+            fallbackColor: currentUserDetails?.fallbackColor || null,
+            createdAt: currentUserDetails?.createdAt || new Date().toISOString(),
+            isClientAccessLimited: false,
+            companyAccessList: null,
+          },
+          createdAt: new Date().toISOString(),
+        }
+
+    let optimisticData
+    if (postCommentPayload.parentId) {
+      optimisticData = activities
+        ? activities.data.map((comment: LogResponse) => {
+            if (comment.details.id === postCommentPayload.parentId) {
+              const updatedReplies = [
+                ...(comment.details.replies as ReplyResponse[]),
+                { ...tempLog, parentId: postCommentPayload.parentId },
+              ]
+
+              return {
+                ...comment,
+                replies: updatedReplies,
+                details: {
+                  ...comment.details,
+                  replies: updatedReplies,
+                },
+              }
+            }
+            return comment
path: src/app/detail/ui/ActivityWrapper.tsx
user: login: rrojan
body: Please extract these into functions / add docs. Let's keep code clean where possible..
created_at: 2025-03-07T12:41:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,8 +187,29 @@ export const ActivityWrapper = ({
   }
 
   // Handle comment deletion
-  const handleDeleteComment = async (commentId: string, logId: string) => {
-    const optimisticData = activities ? activities.data.filter((comment: LogResponse) => comment.id !== logId) : []
+  const handleDeleteComment = async (commentId: string, logId: string, replyId?: string) => {
+    let optimisticData
+    if (replyId) {
+      optimisticData = activities
+        ? activities.data.map((item: LogResponse) => {
+            if (item.id === logId) {
+              const updatedReplies = (item.details.replies as ReplyResponse[]).filter(
+                (reply: ReplyResponse) => reply.id !== replyId,
+              )
+              return {
+                ...item,
+                details: {
+                  ...item.details,
+                  replies: updatedReplies,
+                },
+              }
+            }
+            return item
+          })
+        : []
+    } else {
path: src/app/detail/ui/ActivityWrapper.tsx
user: login: arpandhakal
body: We are returning value to update a variable, so I believe else block is required here. 
created_at: 2025-03-10T08:33:08Z
author_association: COLLABORATOR

PR comments:
user: login: linear[bot]
created_at: 2025-03-06T12:36:18Z
body: <p><a href="https://linear.app/copilotplatforms/issue/OUT-1487/add-support-for-optimistic-update-and-animations-for-replies-while">OUT-1487 Add support for optimistic update and animations for replies while creating/deleting</a></p>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-06T12:36:20Z
body: [vc]: #OCuxziyXMMRB0CJazw0sPIXpDstY1uTJUYYH4R7cyUA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0YXNrcy1hcHAiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vY29waWxvdC1wbGF0Zm9ybXMvdGFza3MtYXBwL0VoVHdLNzJZVU5MbXVYalgzM0t2MjZSODY1NmciLCJwcmV2aWV3VXJsIjoidGFza3MtYXBwLWdpdC1vdXQtMTQ4Ny1jb3BpbG90LXBsYXRmb3Jtcy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InRhc2tzLWFwcC1naXQtb3V0LTE0ODctY29waWxvdC1wbGF0Zm9ybXMudmVyY2VsLmFwcCJ9LCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **tasks-app** | ✅ Ready ([Inspect](https://vercel.com/copilot-platforms/tasks-app/EhTwK72YUNLmuXjX33Kv26R8656g)) | [Visit Preview](https://tasks-app-git-out-1487-copilot-platforms.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/tasks-app-git-out-1487-copilot-platforms.vercel.app?via=pr-comment-feedback-link) | Mar 7, 2025 0:20am |


author_association: CONTRIBUTOR