1- import { useEffect , useState } from "react" ;
1+ import type { ReactNode } from "react" ;
2+ import { useCallback , useEffect , useState } from "react" ;
23import { observer } from "mobx-react" ;
34import { usePathname } from "next/navigation" ;
45import { Globe2 , Lock } from "lucide-react" ;
@@ -7,24 +8,33 @@ import type { EditorRefApi } from "@plane/editor";
78import { useHashScroll } from "@plane/hooks" ;
89import { EIssueCommentAccessSpecifier } from "@plane/types" ;
910import type { TCommentsOperations , TIssueComment } from "@plane/types" ;
10- import { cn } from "@plane/utils" ;
11+ import { calculateTimeAgo , cn , getFileURL , renderFormattedDate , renderFormattedTime } from "@plane/utils" ;
1112// components
1213import { LiteTextEditor } from "@/components/editor/lite-text" ;
1314// local imports
1415import { CommentReactions } from "../comment-reaction" ;
16+ import { CommentCardEditForm } from "./edit-form" ;
17+ import { EmojiReactionButton , EmojiReactionPicker } from "@plane/propel/emoji-reaction" ;
18+ import { Avatar , Tooltip } from "@plane/ui" ;
19+ import { useMember } from "@/hooks/store/use-member" ;
1520
16- type Props = {
21+ export type TCommentCardDisplayProps = {
1722 activityOperations : TCommentsOperations ;
1823 comment : TIssueComment ;
1924 disabled : boolean ;
25+ entityId : string ;
2026 projectId ?: string ;
2127 readOnlyEditorRef : React . RefObject < EditorRefApi > ;
2228 showAccessSpecifier : boolean ;
2329 workspaceId : string ;
2430 workspaceSlug : string ;
31+ isEditing ?: boolean ;
32+ setIsEditing ?: ( isEditing : boolean ) => void ;
33+ renderFooter ?: ( ReactionsComponent : ReactNode | null ) => ReactNode ;
34+ renderQuickActions ?: ( ) => ReactNode ;
2535} ;
2636
27- export const CommentCardDisplay = observer ( function CommentCardDisplay ( props : Props ) {
37+ export const CommentCardDisplay = observer ( function CommentCardDisplay ( props : TCommentCardDisplayProps ) {
2838 const {
2939 activityOperations,
3040 comment,
@@ -34,13 +44,34 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
3444 showAccessSpecifier,
3545 workspaceId,
3646 workspaceSlug,
47+ isEditing = false ,
48+ setIsEditing,
49+ renderFooter,
50+ renderQuickActions,
3751 } = props ;
3852 // states
3953 const [ highlightClassName , setHighlightClassName ] = useState ( "" ) ;
54+ // state
55+ const [ isPickerOpen , setIsPickerOpen ] = useState ( false ) ;
56+ // store hooks
57+ const { getUserDetails } = useMember ( ) ;
58+ // derived values
59+ const userDetails = getUserDetails ( comment ?. actor ) ;
60+ const displayName = comment ?. actor_detail ?. is_bot
61+ ? comment ?. actor_detail ?. first_name + `Bot`
62+ : ( userDetails ?. display_name ?? comment ?. actor_detail ?. display_name ) ;
63+ const avatarUrl = userDetails ?. avatar_url ?? comment ?. actor_detail ?. avatar_url ;
64+
65+ const userReactions = activityOperations . userReactions ( comment . id ) ;
66+
4067 // navigation
4168 const pathname = usePathname ( ) ;
4269 // derived values
4370 const commentBlockId = `comment-${ comment ?. id } ` ;
71+ // Check if there are any reactions to determine if we should render the footer
72+ const reactionIds = activityOperations . reactionIds ( comment . id ) ;
73+ const hasReactions = reactionIds && Object . keys ( reactionIds ) . some ( ( key ) => reactionIds [ key ] ?. length > 0 ) ;
74+
4475 // scroll to comment
4576 const { isHashMatch } = useHashScroll ( {
4677 elementId : commentBlockId ,
@@ -57,6 +88,17 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
5788 return ( ) => clearTimeout ( timeout ) ;
5889 } , [ isHashMatch ] ) ;
5990
91+ const handleEmojiSelect = useCallback (
92+ ( emoji : string ) => {
93+ if ( ! userReactions ) return ;
94+ // emoji is already in decimal string format from EmojiReactionPicker
95+ void activityOperations . react ( comment . id , emoji , userReactions ) ;
96+ } ,
97+ [ activityOperations , comment . id , userReactions ]
98+ ) ;
99+
100+ const shouldRenderReactions = hasReactions && ! disabled ;
101+
60102 return (
61103 < div id = { commentBlockId } className = "relative flex flex-col gap-2" >
62104 { showAccessSpecifier && (
@@ -68,20 +110,74 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
68110 ) }
69111 </ div >
70112 ) }
71- < LiteTextEditor
72- editable = { false }
73- ref = { readOnlyEditorRef }
74- id = { comment . id }
75- initialValue = { comment . comment_html ?? "" }
76- workspaceId = { workspaceId }
77- workspaceSlug = { workspaceSlug }
78- containerClassName = { cn ( "!py-1 transition-[border-color] duration-500" , highlightClassName ) }
79- projectId = { projectId ?. toString ( ) }
80- displayConfig = { {
81- fontSize : "small-font" ,
82- } }
83- />
84- < CommentReactions comment = { comment } disabled = { disabled } activityOperations = { activityOperations } />
113+ < div className = "flex relative w-full gap-2 items-center mb-3" >
114+ < Avatar size = "sm" name = { displayName } src = { getFileURL ( avatarUrl ) } className = "shrink-0" />
115+ < div className = "flex-1 flex flex-wrap items-center gap-1" >
116+ < div className = "text-caption-sm-medium" > { displayName } </ div >
117+ < div className = "text-caption-sm-regular text-tertiary" >
118+ commented{ " " }
119+ < Tooltip
120+ tooltipContent = { `${ renderFormattedDate ( comment . created_at ) } at ${ renderFormattedTime ( comment . created_at ) } ` }
121+ position = "bottom"
122+ >
123+ < span className = "text-tertiary" >
124+ { calculateTimeAgo ( comment . created_at ) }
125+ { comment . edited_at && " (edited)" }
126+ </ span >
127+ </ Tooltip >
128+ </ div >
129+ </ div >
130+ { ! disabled && (
131+ < div className = "flex items-center gap-1 shrink-0" >
132+ < EmojiReactionPicker
133+ isOpen = { isPickerOpen }
134+ handleToggle = { setIsPickerOpen }
135+ onChange = { handleEmojiSelect }
136+ disabled = { disabled }
137+ label = { < EmojiReactionButton onAddReaction = { ( ) => setIsPickerOpen ( true ) } /> }
138+ placement = "bottom-start"
139+ />
140+ { renderQuickActions ? renderQuickActions ( ) : null }
141+ </ div >
142+ ) }
143+ </ div >
144+ { isEditing && setIsEditing ? (
145+ < CommentCardEditForm
146+ activityOperations = { activityOperations }
147+ comment = { comment }
148+ isEditing = { isEditing }
149+ readOnlyEditorRef = { readOnlyEditorRef . current }
150+ setIsEditing = { setIsEditing }
151+ projectId = { projectId }
152+ workspaceId = { workspaceId }
153+ workspaceSlug = { workspaceSlug }
154+ />
155+ ) : (
156+ < >
157+ < LiteTextEditor
158+ editable = { false }
159+ ref = { readOnlyEditorRef }
160+ id = { comment . id }
161+ initialValue = { comment . comment_html ?? "" }
162+ workspaceId = { workspaceId }
163+ workspaceSlug = { workspaceSlug }
164+ containerClassName = { cn ( "!py-1 transition-[border-color] duration-500" , highlightClassName ) }
165+ projectId = { projectId ?. toString ( ) }
166+ displayConfig = { {
167+ fontSize : "small-font" ,
168+ } }
169+ parentClassName = "border-none"
170+ />
171+ { shouldRenderReactions &&
172+ ( renderFooter ? (
173+ renderFooter (
174+ < CommentReactions comment = { comment } disabled = { disabled } activityOperations = { activityOperations } />
175+ )
176+ ) : (
177+ < CommentReactions comment = { comment } disabled = { disabled } activityOperations = { activityOperations } />
178+ ) ) }
179+ </ >
180+ ) }
85181 </ div >
86182 ) ;
87183} ) ;
0 commit comments