diff --git a/web/src/components/MemoReactionListView/MemoReactionListView.tsx b/web/src/components/MemoReactionListView/MemoReactionListView.tsx index 77eb916aa..c0a95833f 100644 --- a/web/src/components/MemoReactionListView/MemoReactionListView.tsx +++ b/web/src/components/MemoReactionListView/MemoReactionListView.tsx @@ -2,17 +2,17 @@ import { observer } from "mobx-react-lite"; import { memo } from "react"; import useCurrentUser from "@/hooks/useCurrentUser"; import { State } from "@/types/proto/api/v1/common"; -import { ReactionSelector, ReactionView } from "../reactions"; +import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service"; import { useReactionGroups } from "./hooks"; -import type { MemoReactionListViewProps } from "./types"; +import ReactionSelector from "./ReactionSelector"; +import ReactionView from "./ReactionView"; -/** - * MemoReactionListView displays the reactions on a memo: - * - Groups reactions by type - * - Shows reaction emoji with count - * - Allows adding new reactions (if not readonly) - */ -const MemoReactionListView = observer((props: MemoReactionListViewProps) => { +interface Props { + memo: Memo; + reactions: Reaction[]; +} + +const MemoReactionListView = observer((props: Props) => { const { memo: memoData, reactions } = props; const currentUser = useCurrentUser(); const reactionGroup = useReactionGroups(reactions); diff --git a/web/src/components/reactions/ReactionSelector.tsx b/web/src/components/MemoReactionListView/ReactionSelector.tsx similarity index 81% rename from web/src/components/reactions/ReactionSelector.tsx rename to web/src/components/MemoReactionListView/ReactionSelector.tsx index 91c6eb267..db3acc8b1 100644 --- a/web/src/components/reactions/ReactionSelector.tsx +++ b/web/src/components/MemoReactionListView/ReactionSelector.tsx @@ -1,26 +1,26 @@ import { SmilePlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { instanceStore } from "@/store"; +import type { Memo } from "@/types/proto/api/v1/memo_service"; import { useReactionActions } from "./hooks"; -import type { ReactionSelectorProps } from "./types"; -/** - * ReactionSelector component provides a popover for selecting emoji reactions - */ -const ReactionSelector = observer((props: ReactionSelectorProps) => { +interface Props { + memo: Memo; + className?: string; + onOpenChange?: (open: boolean) => void; +} + +const ReactionSelector = observer((props: Props) => { const { memo, className, onOpenChange } = props; const [open, setOpen] = useState(false); - const handleOpenChange = useCallback( - (newOpen: boolean) => { - setOpen(newOpen); - onOpenChange?.(newOpen); - }, - [onOpenChange], - ); + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + onOpenChange?.(newOpen); + }; const { hasReacted, handleReactionClick } = useReactionActions({ memo, diff --git a/web/src/components/reactions/ReactionView.tsx b/web/src/components/MemoReactionListView/ReactionView.tsx similarity index 87% rename from web/src/components/reactions/ReactionView.tsx rename to web/src/components/MemoReactionListView/ReactionView.tsx index 16ce947e6..f8d20367c 100644 --- a/web/src/components/reactions/ReactionView.tsx +++ b/web/src/components/MemoReactionListView/ReactionView.tsx @@ -3,14 +3,17 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import useCurrentUser from "@/hooks/useCurrentUser"; import { cn } from "@/lib/utils"; import { State } from "@/types/proto/api/v1/common"; +import type { Memo } from "@/types/proto/api/v1/memo_service"; +import type { User } from "@/types/proto/api/v1/user_service"; import { formatReactionTooltip, useReactionActions } from "./hooks"; -import type { ReactionViewProps } from "./types"; -/** - * ReactionView component displays a single reaction pill with count - * Clicking toggles the reaction for the current user - */ -const ReactionView = observer((props: ReactionViewProps) => { +interface Props { + memo: Memo; + reactionType: string; + users: User[]; +} + +const ReactionView = observer((props: Props) => { const { memo, reactionType, users } = props; const currentUser = useCurrentUser(); const hasReaction = users.some((user) => currentUser && user.username === currentUser.username); diff --git a/web/src/components/MemoReactionListView/hooks.ts b/web/src/components/MemoReactionListView/hooks.ts index 6bfbbf100..a89f00317 100644 --- a/web/src/components/MemoReactionListView/hooks.ts +++ b/web/src/components/MemoReactionListView/hooks.ts @@ -1,32 +1,77 @@ import { uniq } from "lodash-es"; import { useEffect, useState } from "react"; -import { userStore } from "@/store"; -import type { Reaction } from "@/types/proto/api/v1/memo_service"; +import { memoServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { memoStore, userStore } from "@/store"; +import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service"; import type { User } from "@/types/proto/api/v1/user_service"; -import type { ReactionGroup } from "./types"; -/** - * Hook for grouping reactions by type and fetching user data - */ +export type ReactionGroup = Map; + export const useReactionGroups = (reactions: Reaction[]): ReactionGroup => { const [reactionGroup, setReactionGroup] = useState(new Map()); useEffect(() => { const fetchReactionGroups = async () => { const newReactionGroup = new Map(); - for (const reaction of reactions) { const user = await userStore.getOrFetchUserByName(reaction.creator); const users = newReactionGroup.get(reaction.reactionType) || []; users.push(user); newReactionGroup.set(reaction.reactionType, uniq(users)); } - setReactionGroup(newReactionGroup); }; - fetchReactionGroups(); }, [reactions]); return reactionGroup; }; + +interface UseReactionActionsOptions { + memo: Memo; + onComplete?: () => void; +} + +export const useReactionActions = ({ memo, onComplete }: UseReactionActionsOptions) => { + const currentUser = useCurrentUser(); + + const hasReacted = (reactionType: string) => { + return memo.reactions.some((r) => r.reactionType === reactionType && r.creator === currentUser?.name); + }; + + const handleReactionClick = async (reactionType: string) => { + if (!currentUser) return; + + try { + if (hasReacted(reactionType)) { + const reactions = memo.reactions.filter( + (reaction) => reaction.reactionType === reactionType && reaction.creator === currentUser.name, + ); + for (const reaction of reactions) { + await memoServiceClient.deleteMemoReaction({ name: reaction.name }); + } + } else { + await memoServiceClient.upsertMemoReaction({ + name: memo.name, + reaction: { contentId: memo.name, reactionType }, + }); + } + await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true }); + } catch { + // skip error + } + onComplete?.(); + }; + + return { hasReacted, handleReactionClick }; +}; + +export const formatReactionTooltip = (users: User[], reactionType: string): string => { + if (users.length === 0) return ""; + const formatUserName = (user: User) => user.displayName || user.username; + if (users.length < 5) { + return `${users.map(formatUserName).join(", ")} reacted with ${reactionType.toLowerCase()}`; + } + return `${users.slice(0, 4).map(formatUserName).join(", ")} and ${users.length - 4} more reacted with ${reactionType.toLowerCase()}`; +}; diff --git a/web/src/components/MemoReactionListView/index.ts b/web/src/components/MemoReactionListView/index.ts index 4dfc8611b..03557a257 100644 --- a/web/src/components/MemoReactionListView/index.ts +++ b/web/src/components/MemoReactionListView/index.ts @@ -1,3 +1,3 @@ -export { useReactionGroups } from "./hooks"; export { default, default as MemoReactionListView } from "./MemoReactionListView"; -export type { MemoReactionListViewProps, ReactionGroup } from "./types"; +export { default as ReactionSelector } from "./ReactionSelector"; +export { default as ReactionView } from "./ReactionView"; diff --git a/web/src/components/MemoReactionListView/types.ts b/web/src/components/MemoReactionListView/types.ts deleted file mode 100644 index bd5c717bf..000000000 --- a/web/src/components/MemoReactionListView/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service"; -import type { User } from "@/types/proto/api/v1/user_service"; - -/** - * Props for MemoReactionListView component - */ -export interface MemoReactionListViewProps { - /** The memo that reactions belong to */ - memo: Memo; - /** List of reactions to display */ - reactions: Reaction[]; -} - -/** - * Grouped reactions with users who reacted - */ -export type ReactionGroup = Map; diff --git a/web/src/components/MemoView/components/MemoHeader.tsx b/web/src/components/MemoView/components/MemoHeader.tsx index cc2c5fa0d..b4e667b61 100644 --- a/web/src/components/MemoView/components/MemoHeader.tsx +++ b/web/src/components/MemoView/components/MemoHeader.tsx @@ -8,7 +8,7 @@ import type { User } from "@/types/proto/api/v1/user_service"; import { useTranslate } from "@/utils/i18n"; import { convertVisibilityToString } from "@/utils/memo"; import MemoActionMenu from "../../MemoActionMenu"; -import { ReactionSelector } from "../../reactions"; +import { ReactionSelector } from "../../MemoReactionListView"; import UserAvatar from "../../UserAvatar"; import VisibilityIcon from "../../VisibilityIcon"; import { useMemoViewContext } from "../MemoViewContext"; diff --git a/web/src/components/reactions/hooks.ts b/web/src/components/reactions/hooks.ts deleted file mode 100644 index 21af37790..000000000 --- a/web/src/components/reactions/hooks.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useCallback } from "react"; -import { memoServiceClient } from "@/grpcweb"; -import useCurrentUser from "@/hooks/useCurrentUser"; -import { memoStore } from "@/store"; -import type { Memo } from "@/types/proto/api/v1/memo_service"; -import type { User } from "@/types/proto/api/v1/user_service"; - -interface UseReactionActionsOptions { - memo: Memo; - onComplete?: () => void; -} - -/** - * Hook for handling reaction add/remove operations - */ -export const useReactionActions = ({ memo, onComplete }: UseReactionActionsOptions) => { - const currentUser = useCurrentUser(); - - const hasReacted = useCallback( - (reactionType: string) => { - return memo.reactions.some((r) => r.reactionType === reactionType && r.creator === currentUser?.name); - }, - [memo.reactions, currentUser?.name], - ); - - const handleReactionClick = useCallback( - async (reactionType: string) => { - if (!currentUser) return; - - try { - if (hasReacted(reactionType)) { - const reactions = memo.reactions.filter( - (reaction) => reaction.reactionType === reactionType && reaction.creator === currentUser.name, - ); - for (const reaction of reactions) { - await memoServiceClient.deleteMemoReaction({ name: reaction.name }); - } - } else { - await memoServiceClient.upsertMemoReaction({ - name: memo.name, - reaction: { - contentId: memo.name, - reactionType, - }, - }); - } - await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true }); - } catch { - // skip error - } - onComplete?.(); - }, - [memo, currentUser, hasReacted, onComplete], - ); - - return { - hasReacted, - handleReactionClick, - }; -}; - -/** - * Format users list for tooltip display - */ -export const formatReactionTooltip = (users: User[], reactionType: string): string => { - if (users.length === 0) { - return ""; - } - - const formatUserName = (user: User) => user.displayName || user.username; - - if (users.length < 5) { - return `${users.map(formatUserName).join(", ")} reacted with ${reactionType.toLowerCase()}`; - } - - return `${users.slice(0, 4).map(formatUserName).join(", ")} and ${users.length - 4} more reacted with ${reactionType.toLowerCase()}`; -}; diff --git a/web/src/components/reactions/index.ts b/web/src/components/reactions/index.ts deleted file mode 100644 index 6898725f9..000000000 --- a/web/src/components/reactions/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Reaction components for memos - * - * This module provides components for displaying and managing reactions on memos: - * - ReactionSelector: Popover for selecting emoji reactions - * - ReactionView: Display a single reaction with count and tooltip - */ - -export { formatReactionTooltip, useReactionActions } from "./hooks"; -export { default as ReactionSelector } from "./ReactionSelector"; -export { default as ReactionView } from "./ReactionView"; -export type { ReactionSelectorProps, ReactionViewProps } from "./types"; diff --git a/web/src/components/reactions/types.ts b/web/src/components/reactions/types.ts deleted file mode 100644 index 9e97ed4c7..000000000 --- a/web/src/components/reactions/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Memo } from "@/types/proto/api/v1/memo_service"; -import type { User } from "@/types/proto/api/v1/user_service"; - -/** - * Props for ReactionSelector component - */ -export interface ReactionSelectorProps { - /** The memo to add reactions to */ - memo: Memo; - /** Additional CSS classes */ - className?: string; - /** Callback when popover open state changes */ - onOpenChange?: (open: boolean) => void; -} - -/** - * Props for ReactionView component - */ -export interface ReactionViewProps { - /** The memo that the reaction belongs to */ - memo: Memo; - /** The emoji/reaction type */ - reactionType: string; - /** Users who added this reaction */ - users: User[]; -}