Skip to content

Commit

Permalink
Merge pull request #870 from FleetAdmiralJakob/reaction-rewrite
Browse files Browse the repository at this point in the history
fix: rewrote reactions to simplify code and to fix bugs
  • Loading branch information
FleetAdmiralJakob authored Jan 4, 2025
2 parents d82f545 + 65f4904 commit d293c9f
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 264 deletions.
6 changes: 6 additions & 0 deletions .idea/GitLink.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/codestream.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "next build",
"dev": "npm-run-all --parallel next:dev convex:dev ",
"convex:dev": "convex dev --tail-logs",
"next:dev": "next dev --turbo",
"next:dev": "next dev --turbo --experimental-https",
"format": "prettier --check . --ignore-path .gitignore --ignore-path .prettierignore",
"lint": "next lint",
"start": "next start",
Expand Down
139 changes: 67 additions & 72 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

212 changes: 26 additions & 186 deletions src/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import { useUser } from "@clerk/nextjs";
import { useFloating, type ReferenceType } from "@floating-ui/react";
import { useLongPress } from "@reactuses/core";
import { useQueryWithStatus } from "~/app/convex-client-provider";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import { useMutation } from "convex/react";
import { type FunctionReturnType } from "convex/server";
Expand All @@ -25,19 +20,30 @@ import {
Reply,
Trash2,
} from "lucide-react";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect } from "react";
import { createPortal } from "react-dom";
import { useInView } from "react-intersection-observer";
import { toast } from "sonner";
import { api } from "../../convex/_generated/api";
import type { Doc, Id } from "../../convex/_generated/dataModel";
import type { Id } from "../../convex/_generated/dataModel";
import { ReactionHandler } from "./reactions";

dayjs.extend(relativeTime);

export type Message = NonNullable<
FunctionReturnType<typeof api.messages.getMessages>
>[number];

export type UserInfos = [
FunctionReturnType<typeof api.users.getUserData> | undefined,
(
| undefined
| NonNullable<
FunctionReturnType<typeof api.chats.getChatInfoFromId>
>["otherUser"]
),
];

const EditedLabel = ({ message }: { message: Message }) => (
<div className="mr-2 text-[75%] font-bold text-secondary-foreground">
{message.type === "message" && message.modified && "Edited"}
Expand All @@ -63,136 +69,6 @@ const ReplyToMessage = ({ message }: { message: Message }) => {
}
};

const ReactionQuickView = ({
reactions,
}: {
reactions: Doc<"reactions">[];
}) => {
const [animatingEmojis, setAnimatingEmojis] = useState<Set<string>>(
new Set(),
);
const previousReactions = useRef<string[]>([]);
const isFirstRender = useRef(true);

useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
previousReactions.current = reactions.map((r) => r.emoji);
return; // Skip animation logic on first render
}

const currentReactions = reactions.map((r) => r.emoji);
const newReactions = currentReactions.filter(
(emoji) => !previousReactions.current.includes(emoji),
);

if (newReactions.length > 0) {
setAnimatingEmojis(new Set(newReactions));
const timeoutId = setTimeout(() => {
setAnimatingEmojis(new Set());
}, 500); // Match this with your animation duration
// The setTimeout should be cleaned up when the component unmounts or when new reactions are added before the animation completes.
return () => clearTimeout(timeoutId);
}

previousReactions.current = currentReactions;
}, [reactions]);

return (
reactions
// Reduce the reactions array to count occurrences of each emoji
// acc: accumulator array of {emoji, count} objects
// reaction: current reaction being processed
// Returns: array of unique emojis with their counts
.reduce(
(acc, reaction) => {
const existingReaction = acc.find((r) => r.emoji === reaction.emoji);
if (existingReaction) {
existingReaction.count++;
} else {
acc.push({
emoji: reaction.emoji,
count: 1,
});
}
return acc;
},
[] as { emoji: string; count: number }[],
)
.map((reaction, index) => (
<div
key={index}
className={cn(
"flex items-center justify-center rounded-full bg-primary/20 text-sm",
!isFirstRender.current &&
animatingEmojis.has(reaction.emoji) &&
// Match the duration with the timeout above
"animate-jump duration-500",
)}
>
<span className="flex aspect-square h-6 items-center justify-center pt-0.5">
{reaction.emoji}
</span>
{reaction.count > 1 && (
<span className="pl-1 text-xs text-secondary-foreground">
{reaction.count}
</span>
)}
</div>
))
);
};

const ReactionDetails = ({
reactions,
userInfos,
}: {
reactions: Doc<"reactions">[];
userInfos: [
FunctionReturnType<typeof api.users.getUserData> | undefined,
(
| undefined
| NonNullable<
FunctionReturnType<typeof api.chats.getChatInfoFromId>
>["otherUser"]
),
];
}) => {
// Group reactions by emoji
const reactionsByEmoji = reactions.reduce(
(acc, reaction) => {
(acc[reaction.emoji] = acc[reaction.emoji] ?? []).push(reaction);
return acc;
},
{} as Record<string, typeof reactions>,
);

return (
<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">
<span className="text-xl">{emoji}</span>
<div className="text-sm">
{reactions
.map((reaction) => {
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(", ")}
</div>
</div>
</div>
))}
</div>
);
};

export const Message = ({
message,
selectedMessageId,
Expand All @@ -217,15 +93,7 @@ export const Message = ({
setReplyToMessageId: React.Dispatch<
React.SetStateAction<Id<"messages"> | undefined>
>;
userInfos: [
FunctionReturnType<typeof api.users.getUserData> | undefined,
(
| undefined
| NonNullable<
FunctionReturnType<typeof api.chats.getChatInfoFromId>
>["otherUser"]
),
];
userInfos: UserInfos;
setShowFullEmojiPicker: React.Dispatch<React.SetStateAction<boolean>>;
isInBottomHalf: boolean | null;
refsFullEmojiPicker: {
Expand Down Expand Up @@ -643,26 +511,12 @@ export const Message = ({
)}
</div>

{message.type === "message" &&
message.reactions &&
message.reactions.length > 0 && (
<Popover>
<PopoverTrigger
className={cn(
"absolute bottom-4 right-0 flex -translate-x-[0%] select-none items-center justify-center gap-1 rounded-full bg-secondary px-1 lg:select-auto",
{ "z-50": message._id === selectedMessageId },
)}
>
<ReactionQuickView reactions={message.reactions} />
</PopoverTrigger>
<PopoverContent>
<ReactionDetails
reactions={message.reactions}
userInfos={userInfos}
/>
</PopoverContent>
</Popover>
)}
<ReactionHandler
message={message}
selectedMessageId={selectedMessageId}
userInfos={userInfos}
side="right"
/>

<div className="mr-2 text-[75%] font-bold text-secondary-foreground">
{message.type == "message" && !message.deleted
Expand Down Expand Up @@ -853,26 +707,12 @@ export const Message = ({
)}
</div>

{message.type === "message" &&
message.reactions &&
message.reactions.length > 0 && (
<Popover>
<PopoverTrigger
className={cn(
"absolute bottom-0 left-0 flex select-none items-center justify-center gap-1 rounded-full bg-secondary px-1 lg:select-auto",
{ "z-50": message._id === selectedMessageId },
)}
>
<ReactionQuickView reactions={message.reactions} />
</PopoverTrigger>
<PopoverContent>
<ReactionDetails
reactions={message.reactions}
userInfos={userInfos}
/>
</PopoverContent>
</Popover>
)}
<ReactionHandler
message={message}
selectedMessageId={selectedMessageId}
userInfos={userInfos}
side="left"
/>

{chatContainerElement &&
message._id == selectedMessageId &&
Expand Down
Loading

0 comments on commit d293c9f

Please sign in to comment.