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: { @@ -112,6 +189,7 @@ const ReactionQuickView = ({ const ReactionDetails = ({ reactions, userInfos, // Tuple containing current user data and other chat participants' data + chatId, }: { reactions: Doc<"reactions">[]; userInfos: [ @@ -123,7 +201,10 @@ const ReactionDetails = ({ >["otherUser"] ), ]; + chatId: Id<"privateChats">; }) => { + const reactToMessage = useReactToMessage(chatId, userInfos[0]); + // Group reactions by emoji // Creates object like: { "👍": [reaction1, reaction2], "❤️": [reaction3] } const reactionsByEmoji = reactions.reduce( @@ -140,22 +221,43 @@ const ReactionDetails = ({
{Object.entries(reactionsByEmoji).map(([emoji, reactions]) => (
-
+
r.userId === userInfos[0]?._id) && + "cursor-pointer hover:opacity-70", + )} + onClick={() => { + if (reactions.some((r) => r.userId === userInfos[0]?._id)) { + if (reactions[0]) { + void reactToMessage({ + messageId: reactions[0].messageId, + reaction: emoji, + }); + } + } + }} + > {emoji} -
+
{/* Create comma-separated list of usernames who used this emoji */} - {reactions - .map((reaction) => { - // Find user info either from current user or other chat participants - const user = - userInfos[0]?._id === reaction.userId - ? userInfos[0] - : Array.isArray(userInfos[1]) - ? userInfos[1].find((u) => u._id === reaction.userId) - : userInfos[1]; - return user?.username; - }) - .join(", ")} + + {reactions + .map((reaction) => { + // Find user info either from current user or other chat participants + const user = + userInfos[0]?._id === reaction.userId + ? userInfos[0] + : Array.isArray(userInfos[1]) + ? userInfos[1].find((u) => u._id === reaction.userId) + : userInfos[1]; + return user?.username; + }) + .join(", ")} + + {reactions.some((r) => r.userId === userInfos[0]?._id) && ( + Click to remove + )}