Skip to content

Commit 409a3e8

Browse files
[WEB-5768]chore: updated comment UI #8402
1 parent 313314e commit 409a3e8

File tree

12 files changed

+184
-133
lines changed

12 files changed

+184
-133
lines changed

apps/web/ce/components/comments/comment-block.tsx

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,77 +2,43 @@ import type { ReactNode } from "react";
22
import { useRef } from "react";
33
import { observer } from "mobx-react";
44
// plane imports
5-
import { useTranslation } from "@plane/i18n";
5+
import { CommentReplyIcon } from "@plane/propel/icons";
66
import type { TIssueComment } from "@plane/types";
7-
import { Avatar, Tooltip } from "@plane/ui";
8-
import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
7+
import { cn } from "@plane/utils";
98
// hooks
10-
import { useMember } from "@/hooks/store/use-member";
119

1210
type TCommentBlock = {
1311
comment: TIssueComment;
1412
ends: "top" | "bottom" | undefined;
15-
quickActions: ReactNode;
1613
children: ReactNode;
1714
};
1815

1916
export const CommentBlock = observer(function CommentBlock(props: TCommentBlock) {
20-
const { comment, ends, quickActions, children } = props;
21-
// refs
17+
const { comment, ends, children } = props;
2218
const commentBlockRef = useRef<HTMLDivElement>(null);
23-
// store hooks
24-
const { getUserDetails } = useMember();
25-
// derived values
26-
const userDetails = getUserDetails(comment?.actor);
27-
// translation
28-
const { t } = useTranslation();
29-
30-
const displayName = comment?.actor_detail?.is_bot
31-
? comment?.actor_detail?.first_name + ` ${t("bot")}`
32-
: (userDetails?.display_name ?? comment?.actor_detail?.display_name);
33-
34-
const avatarUrl = userDetails?.avatar_url ?? comment?.actor_detail?.avatar_url;
3519

3620
if (!comment) return null;
37-
3821
return (
3922
<div
23+
id={comment.id}
4024
className={`relative flex gap-3 ${ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`}`}
4125
ref={commentBlockRef}
4226
>
4327
<div
44-
className="absolute left-[13px] top-0 bottom-0 w-px transition-border duration-1000 bg-layer-1"
28+
className="absolute left-[13px] top-0 bottom-0 w-px transition-border duration-1000 bg-layer-3"
4529
aria-hidden
4630
/>
4731
<div
4832
className={cn(
49-
"flex-shrink-0 relative w-7 h-6 rounded-full transition-border duration-1000 flex justify-center items-center z-[3] uppercase font-medium"
33+
"flex-shrink-0 relative w-7 h-7 rounded-lg transition-border duration-1000 flex justify-center items-center z-[3] uppercase shadow-raised-100 bg-layer-2 border border-subtle"
5034
)}
5135
>
52-
<Avatar size="base" name={displayName} src={getFileURL(avatarUrl)} className="flex-shrink-0" />
36+
<CommentReplyIcon width={14} height={14} className="text-secondary" aria-hidden="true" />
5337
</div>
5438
<div className="flex flex-col gap-3 truncate flex-grow">
55-
<div className="flex w-full gap-2">
56-
<div className="flex-1 flex flex-wrap items-center gap-1">
57-
<div className="flex items-center gap-1">
58-
<span className="text-11 font-medium">{displayName}</span>
59-
</div>
60-
<div className="text-11 text-tertiary">
61-
commented{" "}
62-
<Tooltip
63-
tooltipContent={`${renderFormattedDate(comment.created_at)} at ${renderFormattedTime(comment.created_at)}`}
64-
position="bottom"
65-
>
66-
<span className="text-tertiary">
67-
{calculateTimeAgo(comment.created_at)}
68-
{comment.edited_at && ` (${t("edited")})`}
69-
</span>
70-
</Tooltip>
71-
</div>
72-
</div>
73-
<div className="flex-shrink-0 ">{quickActions}</div>
39+
<div className="text-body-sm-regular mb-2 bg-layer-2 border border-subtle shadow-raised-100 rounded-lg p-3">
40+
{children}
7441
</div>
75-
<div className="text-14 mb-2">{children}</div>
7642
</div>
7743
</div>
7844
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./comment-block";
2+
export { CommentCardDisplay } from "@/components/comments/card/display";
Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useEffect, useState } from "react";
1+
import type { ReactNode } from "react";
2+
import { useCallback, useEffect, useState } from "react";
23
import { observer } from "mobx-react";
34
import { usePathname } from "next/navigation";
45
import { Globe2, Lock } from "lucide-react";
@@ -7,24 +8,33 @@ import type { EditorRefApi } from "@plane/editor";
78
import { useHashScroll } from "@plane/hooks";
89
import { EIssueCommentAccessSpecifier } from "@plane/types";
910
import type { TCommentsOperations, TIssueComment } from "@plane/types";
10-
import { cn } from "@plane/utils";
11+
import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
1112
// components
1213
import { LiteTextEditor } from "@/components/editor/lite-text";
1314
// local imports
1415
import { 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

Comments
 (0)