Skip to content

Commit

Permalink
Fix #903
Browse files Browse the repository at this point in the history
  • Loading branch information
FleetAdmiralJakob committed Jan 8, 2025
1 parent 64dc8ce commit a81c5b9
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 87 deletions.
74 changes: 2 additions & 72 deletions src/app/(internal-sites)/chats/[chatId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 });
Expand Down
132 changes: 117 additions & 15 deletions src/components/reactions.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -46,6 +122,7 @@ export const ReactionHandler = (props: {
<PopoverContent>
<ReactionDetails
reactions={message.reactions}
chatId={message.privateChatId}
userInfos={userInfos}
/>
</PopoverContent>
Expand Down Expand Up @@ -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: [
Expand All @@ -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(
Expand All @@ -140,22 +221,43 @@ const ReactionDetails = ({
<div className="flex flex-col gap-2 p-2">
{Object.entries(reactionsByEmoji).map(([emoji, reactions]) => (
<div key={emoji}>
<div className="flex items-center gap-2">
<div
className={cn(
"flex items-center gap-2",
reactions.some((r) => 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,
});
}
}
}}
>
<span className="text-xl">{emoji}</span>
<div className="text-sm">
<div className="flex flex-col text-sm">
{/* 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(", ")}
<span className="font-bold">
{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(", ")}
</span>
{reactions.some((r) => r.userId === userInfos[0]?._id) && (
<span className="opacity-80">Click to remove</span>
)}
</div>
</div>
</div>
Expand Down

0 comments on commit a81c5b9

Please sign in to comment.