Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: plane space comment reactions #2100

Merged
merged 3 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React, { useEffect, useState, useRef } from "react";
import { useForm, Controller } from "react-hook-form";
import React, { useState } from "react";

// mobx
import { observer } from "mobx-react-lite";
// react-hook-form
import { useForm, Controller } from "react-hook-form";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { TipTapEditor } from "components/tiptap";
import { CommentReactions } from "components/issues/peek-overview";
// icons
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// types
import { Comment } from "types/issue";
// components
import { TipTapEditor } from "components/tiptap";

type Props = {
workspaceSlug: string;
Expand Down Expand Up @@ -76,7 +81,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
<>Commented {timeAgo(comment.created_at)}</>
<>commented {timeAgo(comment.created_at)}</>
</p>
</div>
<div className="issue-comments-section p-0">
Expand Down Expand Up @@ -125,6 +130,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
editable={false}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
<CommentReactions commentId={comment.id} projectId={comment.project} />
</div>
</div>
</div>
Expand Down
131 changes: 131 additions & 0 deletions space/components/issues/peek-overview/comment/comment-reactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from "react";

import { useRouter } from "next/router";

// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { ReactionSelector, Tooltip } from "components/ui";
// helpers
import { groupReactions, renderEmoji } from "helpers/emoji.helper";

type Props = {
commentId: string;
projectId: string;
};

export const CommentReactions: React.FC<Props> = observer((props) => {
const { commentId, projectId } = props;

const router = useRouter();
const { workspace_slug } = router.query;

const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();

const peekId = issueDetailsStore.peekId;
const user = userStore.currentUser;

const commentReactions = peekId
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
: [];
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};

const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);

const handleAddReaction = (reactionHex: string) => {
if (!workspace_slug || !projectId || !peekId) return;

issueDetailsStore.addCommentReaction(
workspace_slug.toString(),
projectId.toString(),
peekId,
commentId,
reactionHex
);
};

const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !projectId || !peekId) return;

issueDetailsStore.removeCommentReaction(
workspace_slug.toString(),
projectId.toString(),
peekId,
commentId,
reactionHex
);
};

const handleReactionClick = (reactionHex: string) => {
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);

if (userReaction) handleRemoveReaction(reactionHex);
else handleAddReaction(reactionHex);
};

return (
<div className="flex gap-1.5 items-center mt-2">
<ReactionSelector
onSelect={(value) => {
userStore.requiredLogin(() => {
handleReactionClick(value);
});
}}
position="top"
selected={userReactions?.map((r) => r.reaction)}
size="md"
/>

{Object.keys(groupedReactions || {}).map((reaction) => {
const reactions = groupedReactions?.[reaction] ?? [];
const REACTIONS_LIMIT = 1000;

if (reactions.length > 0)
return (
<Tooltip
key={reaction}
tooltipContent={
<div>
{reactions
.map((r) => r.actor_detail.display_name)
.splice(0, REACTIONS_LIMIT)
.join(", ")}
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
</div>
}
>
<button
type="button"
onClick={() => {
userStore.requiredLogin(() => {
handleReactionClick(reaction);
});
}}
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
commentReactions?.some(
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
)
? "bg-custom-primary-100/10"
: "bg-custom-background-80"
}`}
>
<span>{renderEmoji(reaction)}</span>
<span
className={
commentReactions?.some(
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
)
? "text-custom-primary-100"
: ""
}
>
{groupedReactions?.[reaction].length}{" "}
</span>
</button>
</Tooltip>
);
})}
</div>
);
});
3 changes: 3 additions & 0 deletions space/components/issues/peek-overview/comment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./add-comment";
export * from "./comment-detail-card";
export * from "./comment-reactions";
6 changes: 4 additions & 2 deletions space/components/issues/peek-overview/header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from "react";

// mobx
import { observer } from "mobx-react-lite";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// hooks
Expand Down Expand Up @@ -41,7 +43,7 @@ const peekModes: {
},
];

export const PeekOverviewHeader: React.FC<Props> = (props) => {
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
const { handleClose, issueDetails } = props;

const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
Expand Down Expand Up @@ -137,4 +139,4 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
</div>
</>
);
};
});
3 changes: 1 addition & 2 deletions space/components/issues/peek-overview/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./comment";
export * from "./full-screen-peek-view";
export * from "./header";
export * from "./issue-activity";
Expand All @@ -8,5 +9,3 @@ export * from "./side-peek-view";
export * from "./issue-reaction";
export * from "./issue-vote-reactions";
export * from "./issue-emoji-reactions";
export * from "./comment-detail-card";
export * from "./add-comment";
20 changes: 15 additions & 5 deletions space/components/issues/peek-overview/issue-emoji-reactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,27 @@ export const IssueEmojiReactions: React.FC = observer(() => {
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
const groupedReactions = groupReactions(reactions, "reaction");

const handleReactionSelectClick = (reactionHex: string) => {
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);

const handleAddReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return;
const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
if (userReaction) return;

issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
};

const handleReactionClick = (reactionHex: string) => {
const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return;

issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
};

const handleReactionClick = (reactionHex: string) => {
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);

if (userReaction) handleRemoveReaction(reactionHex);
else handleAddReaction(reactionHex);
};

useEffect(() => {
if (user) return;
userStore.fetchCurrentUser();
Expand All @@ -42,9 +51,10 @@ export const IssueEmojiReactions: React.FC = observer(() => {
<ReactionSelector
onSelect={(value) => {
userStore.requiredLogin(() => {
handleReactionSelectClick(value);
handleReactionClick(value);
});
}}
selected={userReactions?.map((r) => r.reaction)}
size="md"
/>
<div className="flex items-center gap-2 flex-wrap">
Expand Down
13 changes: 8 additions & 5 deletions space/components/ui/reaction-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { Icon } from "components/ui";
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];

interface Props {
size?: "sm" | "md" | "lg";
position?: "top" | "bottom";
onSelect: (emoji: string) => void;
position?: "top" | "bottom";
selected?: string[];
size?: "sm" | "md" | "lg";
}

export const ReactionSelector: React.FC<Props> = (props) => {
const { onSelect, position, size } = props;
const { onSelect, position, selected = [], size } = props;

return (
<Popover className="relative">
Expand Down Expand Up @@ -51,7 +52,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
position === "top" ? "-top-12" : "-bottom-12"
}`}
>
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
<div className="flex gap-x-1">
{reactionEmojis.map((emoji) => (
<button
Expand All @@ -61,7 +62,9 @@ export const ReactionSelector: React.FC<Props> = (props) => {
onSelect(emoji);
closePopover();
}}
className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90"
className={`grid place-items-center select-none rounded-md text-sm p-1 ${
selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80"
}`}
>
{renderEmoji(emoji)}
</button>
Expand Down
43 changes: 33 additions & 10 deletions space/services/issue.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,6 @@ class IssueService extends APIService {
});
}

async getCommentsReactions(workspaceSlug: string, projectId: string, commentId: string): Promise<any> {
return this.get(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}

async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
return this.post(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
Expand Down Expand Up @@ -140,6 +130,39 @@ class IssueService extends APIService {
throw error?.response;
});
}

async createCommentReaction(
workspaceSlug: string,
projectId: string,
commentId: string,
data: {
reaction: string;
}
): Promise<any> {
return this.post(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}

async deleteCommentReaction(
workspaceSlug: string,
projectId: string,
commentId: string,
reactionHex: string
): Promise<any> {
return this.delete(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}

export default IssueService;
Loading
Loading