diff --git a/src/app/(internal-sites)/chats/[chatId]/page.tsx b/src/app/(internal-sites)/chats/[chatId]/page.tsx
index b93183eb..4b881d35 100644
--- a/src/app/(internal-sites)/chats/[chatId]/page.tsx
+++ b/src/app/(internal-sites)/chats/[chatId]/page.tsx
@@ -8,6 +8,7 @@ import { useQueryWithStatus } from "~/app/convex-client-provider";
import ChatsWithSearch from "~/components/chats-with-search";
import { DevMode } from "~/components/dev-mode-info";
import { Message } from "~/components/message";
+import { useReactToMessage } from "~/components/reactions";
import { Avatar, AvatarFallback } from "~/components/ui/avatar";
import Badge from "~/components/ui/badge";
import { Form, FormControl, FormField } from "~/components/ui/form";
@@ -481,78 +482,7 @@ export default function Page() {
middleware: [autoPlacement({ padding: 4 })],
});
- const reactToMessage = useMutation(
- api.messages.reactToMessage,
- ).withOptimisticUpdate((localStore, args) => {
- const messageId = args.messageId;
- const emoji = args.reaction;
-
- if (!userInfo.data) return;
-
- const reaction = {
- _id: crypto.randomUUID() as Id<"reactions">,
- _creationTime: Date.now(),
- messageId,
- userId: userInfo.data._id,
- emoji,
- userInfo,
- };
-
- const existingMessages = localStore.getQuery(api.messages.getMessages, {
- chatId: params.chatId,
- });
-
- if (existingMessages) {
- const updateMessageReactions = (message: Message) => {
- // Skip messages that don't match target message ID or aren't message type
- if (message._id !== messageId || message.type !== "message") {
- return message;
- }
-
- // Check if user already has the exact same emoji reaction
- const existingReaction = message.reactions?.find(
- (r) => r.userId === userInfo.data?._id && r.emoji === emoji,
- );
-
- // If user already reacted with this emoji, remove it (toggle off)
- if (existingReaction) {
- return {
- ...message,
- reactions: message.reactions.filter(
- (r) => !(r.userId === userInfo.data?._id && r.emoji === emoji),
- ),
- };
- }
-
- // Check if user has reacted with a different emoji
- const hasOtherReaction = message.reactions?.find(
- (r) => r.userId === userInfo.data?._id,
- );
-
- // If user has different reaction, update existing one to new emoji
- if (hasOtherReaction) {
- return {
- ...message,
- reactions: message.reactions.map((r) =>
- r.userId === userInfo.data?._id ? { ...r, emoji } : r,
- ),
- };
- }
-
- // No existing reactions from user - add new reaction
- return {
- ...message,
- reactions: [...(message.reactions || []), reaction],
- };
- };
-
- localStore.setQuery(
- api.messages.getMessages,
- { chatId: params.chatId },
- existingMessages.map(updateMessageReactions),
- );
- }
- });
+ const reactToMessage = useReactToMessage(params.chatId, userInfo.data);
const reactToMessageHandler = (messageId: Id<"messages">, emoji: string) => {
void reactToMessage({ messageId, reaction: emoji });
diff --git a/src/components/reactions.tsx b/src/components/reactions.tsx
index abb32584..1e14ed91 100644
--- a/src/components/reactions.tsx
+++ b/src/components/reactions.tsx
@@ -1,13 +1,89 @@
import { usePrevious } from "~/lib/hooks";
import { cn } from "~/lib/utils";
-import type { api } from "convex/_generated/api";
+import { api } from "convex/_generated/api";
import type { Doc, Id } from "convex/_generated/dataModel";
+import { useMutation } from "convex/react";
import type { FunctionReturnType } from "convex/server";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import type { Message, UserInfos } from "./message";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
+export const useReactToMessage = (chatId: string, userInfo: UserInfos[0]) => {
+ return useMutation(api.messages.reactToMessage).withOptimisticUpdate(
+ (localStore, args) => {
+ const messageId = args.messageId;
+ const emoji = args.reaction;
+
+ if (!userInfo) return;
+
+ const reaction = {
+ _id: crypto.randomUUID() as Id<"reactions">,
+ _creationTime: Date.now(),
+ messageId,
+ userId: userInfo._id,
+ emoji,
+ userInfo,
+ };
+
+ const existingMessages = localStore.getQuery(api.messages.getMessages, {
+ chatId: chatId,
+ });
+
+ if (existingMessages) {
+ const updateMessageReactions = (message: Message) => {
+ // Skip messages that don't match target message ID or aren't message type
+ if (message._id !== messageId || message.type !== "message") {
+ return message;
+ }
+
+ // Check if user already has the exact same emoji reaction
+ const existingReaction = message.reactions?.find(
+ (r) => r.userId === userInfo?._id && r.emoji === emoji,
+ );
+
+ // If user already reacted with this emoji, remove it (toggle off)
+ if (existingReaction) {
+ return {
+ ...message,
+ reactions: message.reactions.filter(
+ (r) => !(r.userId === userInfo?._id && r.emoji === emoji),
+ ),
+ };
+ }
+
+ // Check if user has reacted with a different emoji
+ const hasOtherReaction = message.reactions?.find(
+ (r) => r.userId === userInfo?._id,
+ );
+
+ // If user has different reaction, update existing one to new emoji
+ if (hasOtherReaction) {
+ return {
+ ...message,
+ reactions: message.reactions.map((r) =>
+ r.userId === userInfo?._id ? { ...r, emoji } : r,
+ ),
+ };
+ }
+
+ // No existing reactions from user - add new reaction
+ return {
+ ...message,
+ reactions: [...(message.reactions || []), reaction],
+ };
+ };
+
+ localStore.setQuery(
+ api.messages.getMessages,
+ { chatId: chatId },
+ existingMessages.map(updateMessageReactions),
+ );
+ }
+ },
+ );
+};
+
export const ReactionHandler = (props: {
message: Message;
selectedMessageId: Id<"messages"> | null;
@@ -46,6 +122,7 @@ export const ReactionHandler = (props: {