- {!disabled && (
-
- )}
+ // Transform reactions data to Propel EmojiReactionType format
+ const reactions: EmojiReactionType[] = useMemo(() => {
+ if (!reactionIds) return [];
+
+ return Object.keys(reactionIds)
+ .filter((reaction) => reactionIds[reaction]?.length > 0)
+ .map((reaction) => ({
+ emoji: stringToEmoji(reaction),
+ count: reactionIds[reaction].length,
+ reacted: userReactions.includes(reaction),
+ users: getReactionUsers(reaction),
+ }));
+ }, [reactionIds, userReactions]);
+
+ const handleReactionClick = (emoji: string) => {
+ if (disabled) return;
+ // Convert emoji back to decimal string format for the API
+ const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0));
+ const reactionString = emojiCodePoints.join("-");
+ issueCommentReactionOperations.react(reactionString);
+ };
- {reactionIds &&
- Object.keys(reactionIds || {}).map(
- (reaction) =>
- reactionIds[reaction]?.length > 0 && (
- <>
-
-
-
- >
- )
- )}
+ const handleEmojiSelect = (emoji: string) => {
+ // emoji is already in decimal string format from EmojiReactionPicker
+ issueCommentReactionOperations.react(emoji);
+ };
+
+ return (
+
+ setIsPickerOpen(true)}
+ />
+ }
+ placement="bottom-start"
+ />
);
});
diff --git a/apps/web/core/components/issues/issue-detail/reactions/issue.tsx b/apps/web/core/components/issues/issue-detail/reactions/issue.tsx
index 56b70ba53ce..804e816c251 100644
--- a/apps/web/core/components/issues/issue-detail/reactions/issue.tsx
+++ b/apps/web/core/components/issues/issue-detail/reactions/issue.tsx
@@ -1,20 +1,20 @@
"use client";
import type { FC } from "react";
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import { observer } from "mobx-react";
+import { stringToEmoji } from "@plane/propel/emoji-icon-picker";
+import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction";
+import type { EmojiReactionType } from "@plane/propel/emoji-reaction";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
-import { Tooltip } from "@plane/propel/tooltip";
import type { IUser } from "@plane/types";
// hooks
// ui
import { cn, formatTextList } from "@plane/utils";
// helpers
-import { renderEmoji } from "@/helpers/emoji.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useMember } from "@/hooks/store/use-member";
// types
-import { ReactionSelector } from "./reaction-selector";
export type TIssueReaction = {
workspaceSlug: string;
@@ -27,6 +27,8 @@ export type TIssueReaction = {
export const IssueReaction: FC
= observer((props) => {
const { workspaceSlug, projectId, issueId, currentUser, disabled = false, className = "" } = props;
+ // state
+ const [isPickerOpen, setIsPickerOpen] = useState(false);
// hooks
const {
reaction: { getReactionsByIssueId, reactionsByUser, getReactionById },
@@ -82,7 +84,7 @@ export const IssueReaction: FC = observer((props) => {
[workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions]
);
- const getReactionUsers = (reaction: string): string => {
+ const getReactionUsers = (reaction: string): string[] => {
const reactionUsers = (reactionIds?.[reaction] || [])
.map((reactionId) => {
const reactionDetails = getReactionById(reactionId);
@@ -92,42 +94,53 @@ export const IssueReaction: FC = observer((props) => {
})
.filter((displayName): displayName is string => !!displayName);
- const formattedUsers = formatTextList(reactionUsers);
- return formattedUsers;
+ return reactionUsers;
+ };
+
+ // Transform reactions data to Propel EmojiReactionType format
+ const reactions: EmojiReactionType[] = useMemo(() => {
+ if (!reactionIds) return [];
+
+ return Object.keys(reactionIds)
+ .filter((reaction) => reactionIds[reaction]?.length > 0)
+ .map((reaction) => ({
+ emoji: stringToEmoji(reaction),
+ count: reactionIds[reaction].length,
+ reacted: userReactions.includes(reaction),
+ users: getReactionUsers(reaction),
+ }));
+ }, [reactionIds, userReactions]);
+
+ const handleReactionClick = (emoji: string) => {
+ if (disabled) return;
+ // Convert emoji back to decimal string format for the API
+ const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0));
+ const reactionString = emojiCodePoints.join("-");
+ issueReactionOperations.react(reactionString);
+ };
+
+ const handleEmojiSelect = (emoji: string) => {
+ // emoji is already in decimal string format from EmojiReactionPicker
+ issueReactionOperations.react(emoji);
};
return (
-
- {!disabled && (
-
- )}
- {reactionIds &&
- Object.keys(reactionIds || {}).map(
- (reaction) =>
- reactionIds[reaction]?.length > 0 && (
- <>
-
-
-
- >
- )
- )}
+
+ setIsPickerOpen(true)}
+ />
+ }
+ placement="bottom-start"
+ />
);
});
diff --git a/apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx b/apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx
deleted file mode 100644
index e3d58b50482..00000000000
--- a/apps/web/core/components/issues/issue-detail/reactions/reaction-selector.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Fragment } from "react";
-import { SmilePlus } from "lucide-react";
-import { Popover, Transition } from "@headlessui/react";
-// helper
-import { renderEmoji } from "@/helpers/emoji.helper";
-// icons
-
-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) => {
- const { onSelect, position, size } = props;
-
- return (
-
- {({ open, close: closePopover }) => (
- <>
-
-
-
-
-
-
-
-
-
- {reactionEmojis.map((emoji) => (
-
- ))}
-
-
-
-
- >
- )}
-
- );
-};
diff --git a/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx b/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx
index 1c49a8542e7..fc96163094c 100644
--- a/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx
+++ b/apps/web/core/components/issues/issue-layouts/filters/applied-filters/project.tsx
@@ -1,7 +1,6 @@
import { observer } from "mobx-react";
-import { CloseIcon } from "@plane/propel/icons";
-// components
import { Logo } from "@plane/propel/emoji-icon-picker";
+import { CloseIcon } from "@plane/propel/icons";
// hooks
import { useProject } from "@/hooks/store/use-project";
diff --git a/packages/propel/src/animated-counter/animated-counter.tsx b/packages/propel/src/animated-counter/animated-counter.tsx
index 09432358a2c..2c377fabce2 100644
--- a/packages/propel/src/animated-counter/animated-counter.tsx
+++ b/packages/propel/src/animated-counter/animated-counter.tsx
@@ -8,9 +8,9 @@ export interface AnimatedCounterProps {
}
const sizeClasses = {
- sm: "text-xs h-4 w-4",
- md: "text-sm h-5 w-5",
- lg: "text-base h-6 w-6",
+ sm: "text-xs",
+ md: "text-sm",
+ lg: "text-base",
};
export const AnimatedCounter: React.FC = ({ count, className, size = "md" }) => {
@@ -44,7 +44,7 @@ export const AnimatedCounter: React.FC = ({ count, classNa
const sizeClass = sizeClasses[size];
return (
-
+
{/* Previous number sliding out */}
{isAnimating && (
= (props) => {
side={finalSide}
align={finalAlign}
sideOffset={8}
+ data-prevent-outside-click="true"
>
diff --git a/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx b/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx
index f05c1d52043..c2456b1949f 100644
--- a/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx
+++ b/packages/propel/src/emoji-reaction/emoji-reaction-picker.stories.tsx
@@ -18,6 +18,12 @@ export default meta;
type Story = StoryObj;
export const Default: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Pick Emoji",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState(null);
@@ -46,6 +52,12 @@ export const Default: Story = {
};
export const WithCustomLabel: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Add Reaction",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState(null);
@@ -71,6 +83,12 @@ export const WithCustomLabel: Story = {
};
export const InlineReactions: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Add",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [reactions, setReactions] = useState([
@@ -128,6 +146,12 @@ export const InlineReactions: Story = {
};
export const DifferentPlacements: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Placements",
+ },
render() {
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
@@ -182,6 +206,12 @@ export const DifferentPlacements: Story = {
};
export const SearchDisabled: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "No Search",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState(null);
@@ -207,6 +237,12 @@ export const SearchDisabled: Story = {
};
export const CustomSearchPlaceholder: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Custom Search",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState(null);
@@ -232,6 +268,12 @@ export const CustomSearchPlaceholder: Story = {
};
export const CloseOnSelectDisabled: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Select Multiple",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmojis, setSelectedEmojis] = useState([]);
@@ -279,6 +321,12 @@ export const CloseOnSelectDisabled: Story = {
};
export const InMessageContext: Story = {
+ args: {
+ isOpen: false,
+ handleToggle: () => {},
+ onChange: () => {},
+ label: "Message",
+ },
render() {
const [isOpen, setIsOpen] = useState(false);
const [reactions, setReactions] = useState([
diff --git a/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx b/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx
index 092a388ac60..a2f691f3089 100644
--- a/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx
+++ b/packages/propel/src/emoji-reaction/emoji-reaction-picker.tsx
@@ -70,6 +70,7 @@ export const EmojiReactionPicker: React.FC = (props) =
side={finalSide}
align={finalAlign}
sideOffset={8}
+ data-prevent-outside-click="true"
>
-
- Small
-
-
-
- Medium (default)
-
-
-
- Large
-
-
-
- );
- },
-};
-
export const MultipleReactions: Story = {
+ args: {
+ emoji: "👍",
+ count: 0,
+ },
render() {
const [reactions, setReactions] = useState([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
@@ -137,6 +124,10 @@ export const MultipleReactions: Story = {
};
export const AddButton: Story = {
+ args: {
+ emoji: "➕",
+ count: 0,
+ },
render() {
const handleAdd = () => {
alert("Add reaction clicked");
@@ -146,28 +137,11 @@ export const AddButton: Story = {
},
};
-export const AddButtonSizes: Story = {
- render() {
- return (
-
-
- Small
-
-
-
- Medium
-
-
-
- Large
-
-
-
- );
- },
-};
-
export const ReactionGroup: Story = {
+ args: {
+ emoji: "👍",
+ count: 0,
+ },
render() {
const [reactions, setReactions] = useState([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
@@ -205,34 +179,11 @@ export const ReactionGroup: Story = {
},
};
-export const ReactionGroupSizes: Story = {
- render() {
- const reactions: EmojiReactionType[] = [
- { emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob"] },
- { emoji: "❤️", count: 12, reacted: true, users: ["Charlie", "David"] },
- { emoji: "🎉", count: 3, reacted: false, users: ["Emma"] },
- ];
-
- return (
-
-
- Small
-
-
-
- Medium
-
-
-
- Large
-
-
-
- );
- },
-};
-
export const InMessageContext: Story = {
+ args: {
+ emoji: "👍",
+ count: 0,
+ },
render() {
const [reactions, setReactions] = useState([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
diff --git a/packages/propel/src/emoji-reaction/emoji-reaction.tsx b/packages/propel/src/emoji-reaction/emoji-reaction.tsx
index cad773364f7..898fe894b65 100644
--- a/packages/propel/src/emoji-reaction/emoji-reaction.tsx
+++ b/packages/propel/src/emoji-reaction/emoji-reaction.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
-import { Plus } from "lucide-react";
import { AnimatedCounter } from "../animated-counter";
import { stringToEmoji } from "../emoji-icon-picker";
+import { AddReactionIcon } from "../icons";
import { Tooltip } from "../tooltip";
import { cn } from "../utils";
@@ -20,7 +20,6 @@ export interface EmojiReactionProps extends React.ButtonHTMLAttributes void;
className?: string;
showCount?: boolean;
- size?: "sm" | "md" | "lg";
}
export interface EmojiReactionGroupProps extends React.HTMLAttributes {
@@ -30,46 +29,15 @@ export interface EmojiReactionGroupProps extends React.HTMLAttributes {
onAddReaction?: () => void;
className?: string;
- size?: "sm" | "md" | "lg";
}
-const sizeClasses = {
- sm: {
- button: "px-2 py-1 text-xs gap-1",
- emoji: "text-sm",
- count: "text-xs",
- addButton: "h-6 w-6",
- addIcon: "h-3 w-3",
- },
- md: {
- button: "px-2.5 py-1.5 text-sm gap-1.5",
- emoji: "text-base",
- count: "text-sm",
- addButton: "h-7 w-7",
- addIcon: "h-3.5 w-3.5",
- },
- lg: {
- button: "px-3 py-2 text-base gap-2",
- emoji: "text-lg",
- count: "text-base",
- addButton: "h-8 w-8",
- addIcon: "h-4 w-4",
- },
-};
-
const EmojiReaction = React.forwardRef(
- (
- { emoji, count, reacted = false, users = [], onReactionClick, className, showCount = true, size = "md", ...props },
- ref
- ) => {
- const sizeClass = sizeClasses[size];
-
+ ({ emoji, count, reacted = false, users = [], onReactionClick, className, showCount = true, ...props }, ref) => {
const handleClick = () => {
onReactionClick?.(emoji);
};
@@ -96,9 +64,7 @@ const EmojiReaction = React.forwardRef(
ref={ref}
onClick={handleClick}
className={cn(
- "inline-flex items-center rounded-full border transition-all duration-200 hover:scale-105",
- "focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-1",
- sizeClass.button,
+ "inline-flex items-center rounded-full border px-1.5 text-xs gap-0.5 transition-all duration-200",
reacted
? "bg-custom-primary-100/10 border-custom-primary-100 text-custom-primary-100"
: "bg-custom-background-100 border-custom-border-200 text-custom-text-300 hover:border-custom-border-300 hover:bg-custom-background-90",
@@ -106,8 +72,8 @@ const EmojiReaction = React.forwardRef(
)}
{...props}
>
- {emoji}
- {showCount && count > 0 && }
+ {emoji}
+ {showCount && count > 0 && }
);
@@ -120,42 +86,29 @@ const EmojiReaction = React.forwardRef(
);
const EmojiReactionButton = React.forwardRef(
- ({ onAddReaction, className, size = "md", ...props }, ref) => {
- const sizeClass = sizeClasses[size];
-
- return (
-
- );
- }
+ ({ onAddReaction, className, ...props }, ref) => (
+
+ )
);
const EmojiReactionGroup = React.forwardRef(
(
- {
- reactions,
- onReactionClick,
- onAddReaction,
- className,
- showAddButton = true,
- maxDisplayUsers = 5,
- size = "md",
- ...props
- },
+ { reactions, onReactionClick, onAddReaction, className, showAddButton = true, maxDisplayUsers = 5, ...props },
ref
) => (
@@ -167,10 +120,9 @@ const EmojiReactionGroup = React.forwardRef
))}
- {showAddButton && }
+ {showAddButton && }
)
);
diff --git a/packages/propel/src/icons/actions/add-reaction-icon.tsx b/packages/propel/src/icons/actions/add-reaction-icon.tsx
new file mode 100644
index 00000000000..aeee0e4fb82
--- /dev/null
+++ b/packages/propel/src/icons/actions/add-reaction-icon.tsx
@@ -0,0 +1,21 @@
+import * as React from "react";
+
+import { IconWrapper } from "../icon-wrapper";
+import { ISvgIcons } from "../type";
+
+export const AddReactionIcon: React.FC = ({ color = "currentColor", ...rest }) => {
+ const clipPathId = React.useId();
+
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/propel/src/icons/actions/index.ts b/packages/propel/src/icons/actions/index.ts
index 0a0b7355dea..16971752fe7 100644
--- a/packages/propel/src/icons/actions/index.ts
+++ b/packages/propel/src/icons/actions/index.ts
@@ -1,2 +1,3 @@
export * from "./add-icon";
+export * from "./add-reaction-icon";
export * from "./close-icon";
diff --git a/packages/propel/src/icons/constants.tsx b/packages/propel/src/icons/constants.tsx
index 556a028878a..cbb8299b8a0 100644
--- a/packages/propel/src/icons/constants.tsx
+++ b/packages/propel/src/icons/constants.tsx
@@ -1,6 +1,7 @@
import { Icon } from "./icon";
export const ActionsIconsMap = [
{ icon: , title: "AddIcon" },
+ { icon: , title: "AddReactionIcon" },
{ icon: , title: "CloseIcon" },
];
diff --git a/packages/propel/src/icons/registry.ts b/packages/propel/src/icons/registry.ts
index db44b9ea3ef..96f6e2df662 100644
--- a/packages/propel/src/icons/registry.ts
+++ b/packages/propel/src/icons/registry.ts
@@ -1,3 +1,4 @@
+import { AddReactionIcon } from "./actions";
import { AddIcon } from "./actions/add-icon";
import { CloseIcon } from "./actions/close-icon";
import { ChevronDownIcon } from "./arrows/chevron-down";
@@ -112,6 +113,7 @@ export const ICON_REGISTRY = {
// Action icons
"action.add": AddIcon,
+ "action.add-reaction": AddReactionIcon,
"action.close": CloseIcon,
// Arrow icons