mirror of https://github.com/usememos/memos.git
chore: reorganize reaction components
This commit is contained in:
parent
6dcf7cc74c
commit
07072b75a7
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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);
|
||||
|
|
@ -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<string, User[]>;
|
||||
|
||||
export const useReactionGroups = (reactions: Reaction[]): ReactionGroup => {
|
||||
const [reactionGroup, setReactionGroup] = useState<ReactionGroup>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
const fetchReactionGroups = async () => {
|
||||
const newReactionGroup = new Map<string, User[]>();
|
||||
|
||||
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()}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<string, User[]>;
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
@ -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[];
|
||||
}
|
||||
Loading…
Reference in New Issue