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: issue & comment reaction #1690

Merged
merged 5 commits into from
Jul 27, 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
1 change: 1 addition & 0 deletions apps/app/components/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from "./sidebar";
export * from "./theme";
export * from "./views";
export * from "./feeds";
export * from "./reaction-selector";
export * from "./image-picker-popover";
85 changes: 85 additions & 0 deletions apps/app/components/core/reaction-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Fragment } from "react";

// headless ui
import { Popover, Transition } from "@headlessui/react";

// helper
import { renderEmoji } from "helpers/emoji.helper";

// icons
import { Icon } from "components/ui";

const reactionEmojis = [
"128077",
"128078",
"128516",
"128165",
"128533",
"129505",
"9992",
"128064",
];

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

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

return (
<Popover className="relative">
{({ open, close: closePopover }) => (
<>
<Popover.Button
className={`${
open ? "" : "text-opacity-90"
} group inline-flex items-center rounded-md bg-custom-background-80 focus:outline-none`}
>
<span
className={`flex justify-center items-center rounded-md ${
size === "sm" ? "w-6 h-6" : size === "md" ? "w-8 h-8" : "w-10 h-10"
}`}
>
<Icon iconName="add_reaction" className="text-custom-text-100 scale-125" />
</span>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel
className={`absolute -left-2 z-10 ${position === "top" ? "-top-12" : "-bottom-12"}`}
>
<div className="bg-custom-background-0 border rounded-md px-2 py-1.5">
<div className="flex gap-x-2">
{reactionEmojis.map((emoji) => (
<button
key={emoji}
type="button"
onClick={() => {
onSelect(emoji);
closePopover();
}}
className="flex h-5 w-5 select-none items-center justify-between text-sm"
>
{renderEmoji(emoji)}
</button>
))}
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
};
8 changes: 8 additions & 0 deletions apps/app/components/inbox/inbox-main-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
IssueActivitySection,
IssueDescriptionForm,
IssueDetailsSidebar,
IssueReaction,
} from "components/issues";
// ui
import { Loader } from "components/ui";
Expand Down Expand Up @@ -303,6 +304,13 @@ export const InboxMainContent: React.FC = () => {
}
/>
</div>

<IssueReaction
projectId={projectId}
workspaceSlug={workspaceSlug}
issueId={issueDetails.id}
/>

<div className="space-y-5">
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
<IssueActivitySection issueId={issueDetails.id} user={user} />
Expand Down
7 changes: 7 additions & 0 deletions apps/app/components/issues/comment/comment-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon } from "@heroicons/rea
import useUser from "hooks/use-user";
// ui
import { CustomMenu } from "components/ui";
import { CommentReaction } from "components/issues";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// types
Expand Down Expand Up @@ -138,6 +139,12 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
ref={showEditorRef}
/>

<CommentReaction
workspaceSlug={comment?.workspace_detail?.slug}
projectId={comment.project}
commentId={comment.id}
/>
</div>
</div>
</div>
Expand Down
80 changes: 80 additions & 0 deletions apps/app/components/issues/comment/comment-reaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from "react";

// hooks
import useUser from "hooks/use-user";
import useCommentReaction from "hooks/use-comment-reaction";
// ui
import { ReactionSelector } from "components/core";
// helper
import { renderEmoji } from "helpers/emoji.helper";

type Props = {
workspaceSlug?: string | string[];
projectId?: string | string[];
commentId: string;
};

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

const { user } = useUser();

const {
commentReactions,
groupedReactions,
handleReactionCreate,
handleReactionDelete,
isLoading,
} = useCommentReaction(workspaceSlug, projectId, commentId);

const handleReactionClick = (reaction: string) => {
if (!workspaceSlug || !projectId || !commentId) return;

const isSelected = commentReactions?.some(
(r) => r.actor === user?.id && r.reaction === reaction
);

if (isSelected) {
handleReactionDelete(reaction);
} else {
handleReactionCreate(reaction);
}
};

return (
<div className="flex gap-1.5 items-center mt-2">
<ReactionSelector
size="md"
position="top"
value={
commentReactions
?.filter((reaction) => reaction.actor === user?.id)
.map((r) => r.reaction) || []
}
onSelect={handleReactionClick}
/>

{Object.keys(groupedReactions || {}).map(
(reaction) =>
groupedReactions?.[reaction]?.length &&
groupedReactions[reaction].length > 0 && (
<button
type="button"
onClick={() => {
handleReactionClick(reaction);
}}
key={reaction}
className={`flex items-center gap-1 text-custom-text-100 h-full px-2 py-1 rounded-md ${
commentReactions?.some((r) => r.actor === user?.id && r.reaction === reaction)
? "bg-custom-primary-100/10"
: "bg-custom-background-80"
}`}
>
<span>{groupedReactions?.[reaction].length} </span>
<span>{renderEmoji(reaction)}</span>
</button>
)
)}
</div>
);
};
1 change: 1 addition & 0 deletions apps/app/components/issues/comment/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./add-comment";
export * from "./comment-card";
export * from "./comment-reaction";
1 change: 1 addition & 0 deletions apps/app/components/issues/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from "./parent-issues-list-modal";
export * from "./sidebar";
export * from "./sub-issues-list";
export * from "./label";
export * from "./issue-reaction";
70 changes: 70 additions & 0 deletions apps/app/components/issues/issue-reaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// hooks
import useUserAuth from "hooks/use-user-auth";
import useIssueReaction from "hooks/use-issue-reaction";
// components
import { ReactionSelector } from "components/core";
// string helpers
import { renderEmoji } from "helpers/emoji.helper";

// types
type Props = {
workspaceSlug?: string | string[];
projectId?: string | string[];
issueId?: string | string[];
};

export const IssueReaction: React.FC<Props> = (props) => {
const { workspaceSlug, projectId, issueId } = props;

const { user } = useUserAuth();

const { reactions, groupedReactions, handleReactionCreate, handleReactionDelete } =
useIssueReaction(workspaceSlug, projectId, issueId);

const handleReactionClick = (reaction: string) => {
if (!workspaceSlug || !projectId || !issueId) return;

const isSelected = reactions?.some((r) => r.actor === user?.id && r.reaction === reaction);

if (isSelected) {
handleReactionDelete(reaction);
} else {
handleReactionCreate(reaction);
}
};

return (
<div className="flex gap-1.5 items-center mt-4">
<ReactionSelector
size="md"
position="top"
value={
reactions?.filter((reaction) => reaction.actor === user?.id).map((r) => r.reaction) || []
}
onSelect={handleReactionClick}
/>

{Object.keys(groupedReactions || {}).map(
(reaction) =>
groupedReactions?.[reaction]?.length &&
groupedReactions[reaction].length > 0 && (
<button
type="button"
onClick={() => {
handleReactionClick(reaction);
}}
key={reaction}
className={`flex items-center gap-1 text-custom-text-100 h-full px-2 py-1 rounded-md ${
reactions?.some((r) => r.actor === user?.id && r.reaction === reaction)
? "bg-custom-primary-100/10"
: "bg-custom-background-80"
}`}
>
<span>{groupedReactions?.[reaction].length} </span>
<span>{renderEmoji(reaction)}</span>
</button>
)
)}
</div>
);
};
4 changes: 4 additions & 0 deletions apps/app/components/issues/main-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
IssueAttachments,
IssueDescriptionForm,
SubIssuesList,
IssueReaction,
} from "components/issues";
// ui
import { CustomMenu } from "components/ui";
Expand Down Expand Up @@ -117,6 +118,9 @@ export const IssueMainContent: React.FC<Props> = ({
handleFormSubmit={submitChanges}
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
/>

<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />

<div className="mt-2 space-y-2">
<SubIssuesList parentIssue={issueDetails} user={user} disabled={uneditable} />
</div>
Expand Down
10 changes: 10 additions & 0 deletions apps/app/constants/fetch-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,13 @@ export const getPaginatedNotificationKey = (
cursor,
})}`;
};

export const ISSUE_REACTION_LIST = (workspaceSlug: string, projectId: string, issueId: string) =>
`ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`;

export const COMMENT_REACTION_LIST = (
workspaceSlug: string,
projectId: string,
commendId: string
) =>
`COMMENT_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${commendId.toUpperCase()}`;
15 changes: 15 additions & 0 deletions apps/app/helpers/emoji.helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ export const renderEmoji = (
);
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
};

export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = (
reactions: any,
key: string
) => {
const groupedReactions = reactions.reduce((acc: any, reaction: any) => {
if (!acc[reaction[key]]) {
acc[reaction[key]] = [];
}
acc[reaction[key]].push(reaction);
return acc;
}, {} as { [key: string]: any[] });

return groupedReactions;
};
Loading