Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add reactions #765

Merged
merged 17 commits into from
Dec 2, 2024
Merged

feat: add reactions #765

merged 17 commits into from
Dec 2, 2024

Conversation

FleetAdmiralJakob
Copy link
Owner

@FleetAdmiralJakob FleetAdmiralJakob commented Dec 1, 2024

closes #672

  • Mobile
  • Animations

Summary by CodeRabbit

Release Notes

  • New Features

    • Users can now react to messages with emojis, enhancing interaction.
    • An EmojiPicker component has been added to select emojis easily.
    • Reactions to messages are displayed in a new ReactionDetails component.
  • Bug Fixes

    • Improved error handling and user feedback for message interactions.
  • Style

    • Updated dark mode styles for better visual consistency across UI elements.
  • Chores

    • Added new dependencies for enhanced emoji handling capabilities.
    • Updated .gitignore to exclude certificates from tracking.

Copy link

vercel bot commented Dec 1, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
chat-io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 2, 2024 6:38am

Copy link
Contributor

coderabbitai bot commented Dec 1, 2024

Caution

Review failed

The pull request is closed.

Walkthrough

The changes introduce a new feature allowing users to react to messages with emojis. A mutation function reactToMessage is added to handle user reactions, including checks for authentication and existing reactions. The getMessages function is updated to return a new reactions field. Additionally, a new reactions entity is added to the schema, linking users and messages. An EmojiPicker component is introduced for selecting emojis, and the Message component is enhanced to display reactions. CSS variables for dark mode are also updated for better visual consistency.

Changes

File Change Summary
convex/messages.ts Added reactToMessage mutation; updated getMessages to include reactions field.
convex/schema.ts Introduced reactions entity; updated users and messages entities to include edges to reactions.
package.json Added dependencies: @emoji-mart/data, @emoji-mart/react, emoji-mart, emoji-regex.
src/app/(internal-sites)/chats/[chatId]/page.tsx Added EmojiPicker component; updated state and functions for handling reactions.
src/components/message.tsx Added ReactionDetails component; updated props and logic for displaying reactions.
src/components/ui/popover.tsx Introduced Popover, PopoverTrigger, and PopoverContent components.
src/styles/globals.css Updated CSS variables for dark mode styles.
.gitignore Added certificates to ignored files.

Assessment against linked issues

Objective Addressed Explanation
React to a message with an emoji

Possibly related PRs

🐰 "In the chat, emojis will play,
A reaction here, a smile today.
With a picker bright and new,
Express your thoughts, it's all for you!
So hop along, let feelings flow,
With every message, let joy grow!" 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 8680de0 and d16d940.

📒 Files selected for processing (1)
  • src/components/message.tsx (6 hunks)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

socket-security bot commented Dec 1, 2024

New dependencies detected. Learn more about Socket for GitHub ↗︎

Package New capabilities Transitives Size Publisher
npm/@emoji-mart/[email protected] None 0 27.9 MB etiennelem
npm/@emoji-mart/[email protected] None 0 7.11 kB etiennelem
npm/@reactuses/[email protected] environment Transitive: eval, network +5 1.34 MB lianwenwu
npm/[email protected] network 0 1.63 MB etiennelem
npm/[email protected] None 0 32 kB google-wombot

View full report↗︎

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (3)
src/app/(internal-sites)/chats/[chatId]/page.tsx (1)

85-85: Remove unnecessary console.log statement

The console.log statement used for debugging should be removed to prevent cluttering the console in production.

Apply this diff to remove the console.log:

- console.log(emoji);
src/components/message.tsx (1)

82-91: Simplify reaction grouping logic in ReactionDetails component

The redundant if check can be removed because the nullish coalescing operator ?? already initializes the array when acc[reaction.emoji] is undefined.

Apply this diff to simplify the grouping of reactions:

- acc[reaction.emoji] = acc[reaction.emoji] ?? [];
- if (acc[reaction.emoji] === undefined) {
-   acc[reaction.emoji] = [];
- }
- acc[reaction.emoji]!.push(reaction);
+ (acc[reaction.emoji] = acc[reaction.emoji] ?? []).push(reaction);
convex/messages.ts (1)

362-366: Use consistent reaction value

The new reaction creation should use the trimmed reaction value consistently.

     const reaction = await ctx.table("reactions").insert({
-      emoji: trimmedReaction,
+      emoji: trimmedReaction,  // This is correct
       userId: convexUser._id,
       messageId,
     });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3738da3 and 5ec36e6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • convex/messages.ts (5 hunks)
  • convex/schema.ts (2 hunks)
  • package.json (2 hunks)
  • src/app/(internal-sites)/chats/[chatId]/page.tsx (8 hunks)
  • src/components/message.tsx (5 hunks)
  • src/components/ui/popover.tsx (1 hunks)
  • src/styles/globals.css (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/message.tsx

[error] 216-218: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)

🔇 Additional comments (5)
src/components/ui/popover.tsx (1)

1-30: Popover component implementation looks good

The Popover, PopoverTrigger, and PopoverContent components are correctly implemented using Radix UI primitives and adhere to best practices.

src/styles/globals.css (1)

Line range hint 55-70: No issues with CSS variable updates for dark mode

The updates to CSS custom properties for dark mode are consistent and enhance visual consistency.

convex/schema.ts (1)

65-68: reactions entity added correctly to the schema

The new reactions entity and its edges are properly defined, integrating well with existing entities.

package.json (1)

20-21: Verify emoji-related dependencies for security and stability

The new emoji-related dependencies use caret versioning (^), which could lead to compatibility issues. Let's verify the versions and check for any security concerns.

Also applies to: 47-48

✅ Verification successful

Dependencies are using latest stable versions with no security vulnerabilities

The verification shows that:

  • @emoji-mart/[email protected] is the latest version
  • @emoji-mart/[email protected] is the latest version
  • No security vulnerabilities were found in any of the emoji-related packages

While caret versioning is used, the packages are already at their latest stable versions, so there's no immediate compatibility risk.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check latest versions and security advisories for emoji-related packages
for pkg in "@emoji-mart/data" "@emoji-mart/react" "emoji-mart" "emoji-regex"; do
  echo "Checking $pkg..."
  # Get latest version
  curl -s "https://registry.npmjs.org/$pkg" | jq -r '.["dist-tags"].latest'
  
  # Check for security advisories
  gh api graphql -f query='{
    securityVulnerabilities(first: 5, ecosystem: NPM, package: "'$pkg'") {
      nodes {
        advisory {
          summary
          severity
          publishedAt
        }
        vulnerableVersionRange
        firstPatchedVersion {
          identifier
        }
      }
    }
  }'
done

Length of output: 2417

convex/messages.ts (1)

35-35: LGTM: Reactions properly included in message structure

The changes correctly fetch and include reactions in the message response.

Also applies to: 67-67

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (3)
src/app/(internal-sites)/chats/[chatId]/page.tsx (3)

59-90: Consider adding error boundaries and organizing props

The EmojiPicker component could benefit from:

  1. Error boundaries to handle potential runtime errors from the third-party picker
  2. Props interface organization using Pick/Omit utility types
+interface EmojiPickerProps {
+  showFullEmojiPicker: boolean;
+  refsFullEmojiPicker: Pick<{
+    setFloating: (ref: HTMLElement | null) => void;
+  }, 'setFloating'>;
+  selectedMessageId: Id<"messages"> | null;
+  floatingStylesFullEmojiPicker: React.CSSProperties;
+  reactToMessageHandler: (messageId: Id<"messages">, emoji: string) => void;
+}

-const EmojiPicker = ({
+const EmojiPicker = React.memo(({
   showFullEmojiPicker,
   refsFullEmojiPicker,
   floatingStylesFullEmojiPicker,
   reactToMessageHandler,
   selectedMessageId,
-}: {
-  showFullEmojiPicker: boolean;
-  refsFullEmojiPicker: {
-    setFloating: (ref: HTMLElement | null) => void;
-  };
-  selectedMessageId: Id<"messages"> | null;
-  floatingStylesFullEmojiPicker: React.CSSProperties;
-  reactToMessageHandler: (messageId: Id<"messages">, emoji: string) => void;
-}) => {
+}: EmojiPickerProps) => {
   if (!showFullEmojiPicker || !selectedMessageId) return null;

   return (
+    <ErrorBoundary fallback={<div>Error loading emoji picker</div>}>
       <div
         ref={refsFullEmojiPicker.setFloating}
         style={floatingStylesFullEmojiPicker}
         className="z-[1000] opacity-100"
       >
         <Picker
           data={data}
           onEmojiSelect={(emoji: { native: string }) => {
             reactToMessageHandler(selectedMessageId, emoji.native);
           }}
         />
       </div>
+    </ErrorBoundary>
   );
-};
+});
+
+EmojiPicker.displayName = 'EmojiPicker';

Line range hint 308-425: Extract emoji picker portal into a reusable component

The portal implementation for the emoji picker is duplicated and could be abstracted into a reusable component.

+const EmojiPickerPortal = ({
+  show,
+  messageId,
+  containerRef,
+  children,
+}: {
+  show: boolean;
+  messageId: Id<"messages">;
+  containerRef: HTMLElement;
+  children: React.ReactNode;
+}) => {
+  if (!show || !messageId) return null;
+  return createPortal(children, containerRef);
+};

-{chatContainerElement &&
-message._id == selectedMessageId &&
-message.type == "message"
-  ? createPortal(
+<EmojiPickerPortal
+  show={message.type === "message"}
+  messageId={message._id}
+  containerRef={chatContainerElement}
+>
     <div
       ref={(ref) => {
         refsEmojiPickerQuickReaction.setFloating(ref);
         refsFullEmojiPicker.setReference(ref);
       }}
       style={floatingStylesEmojiPickerQuickReaction}
       className="z-50 py-3 opacity-100"
     >
       {/* ... emoji picker content ... */}
     </div>
-    chatContainerElement,
-  )
-: null}
+</EmojiPickerPortal>

Line range hint 426-835: Reduce code duplication in message rendering

The message rendering logic is duplicated between sender and receiver views. Consider extracting common functionality into shared components.

+const MessageContent = ({
+  message,
+  userInfos,
+  isSender,
+}: {
+  message: Message;
+  userInfos: UserInfos;
+  isSender: boolean;
+}) => {
+  if (message.type === "message" && message.deleted) {
+    return (
+      <div className="flex font-medium">
+        <Ban />
+        <p className="ml-2.5">This message was deleted</p>
+      </div>
+    );
+  }
+
+  return (
+    <div className="relative">
+      <div>{message.content}</div>
+      {message.reactions && message.reactions.length > 0 && (
+        <ReactionPopover
+          reactions={message.reactions}
+          userInfos={userInfos}
+          isSender={isSender}
+        />
+      )}
+    </div>
+  );
+};

-{message.from.username == clerkUser.user?.username ? (
+const isSender = message.from.username === clerkUser.user?.username;
+return (
   <div
-    className={cn("my-1 flex w-full flex-col items-end", {
+    className={cn("my-1 flex w-full flex-col", {
+      "items-end": isSender,
+      "items-start": !isSender,
       "mr-0 items-center":
         message.type == "pendingRequest" ||
         message.type == "rejectedRequest",
     })}
   >
     <EditedLabel message={message} />
     <ReplyToMessage message={message} />
-    {/* ... rest of the duplicated logic ... */}
+    <MessageContent
+      message={message}
+      userInfos={userInfos}
+      isSender={isSender}
+    />
   </div>
-) : (
-  // ... duplicate code for receiver view ...
-)}
+);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec36e6 and 10e346d.

📒 Files selected for processing (2)
  • src/app/(internal-sites)/chats/[chatId]/page.tsx (8 hunks)
  • src/components/message.tsx (5 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/message.tsx

[error] 83-83: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (2)
src/app/(internal-sites)/chats/[chatId]/page.tsx (1)

532-536: LGTM!

The handler function is concise and effectively manages both the reaction mutation and state cleanup.

src/components/message.tsx (1)

209-210: LGTM! Mobile detection has been simplified

The mobile detection logic has been simplified as suggested in the previous review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (5)
src/app/(internal-sites)/chats/[chatId]/page.tsx (2)

59-90: LGTM! Consider using TypeScript's Pick type for props.

The EmojiPicker component is well-structured with clear responsibilities. The conditional rendering and floating UI integration are implemented correctly.

Consider using TypeScript's Pick type to derive the props interface from the parent component's props:

-}: {
-  showFullEmojiPicker: boolean;
-  refsFullEmojiPicker: {
-    setFloating: (ref: HTMLElement | null) => void;
-  };
-  selectedMessageId: Id<"messages"> | null;
-  floatingStylesFullEmojiPicker: React.CSSProperties;
-  reactToMessageHandler: (messageId: Id<"messages">, emoji: string) => void;
-}
+}: Pick<MessageProps, 'showFullEmojiPicker' | 'refsFullEmojiPicker' | 'selectedMessageId' | 'floatingStylesFullEmojiPicker' | 'reactToMessageHandler'>

471-542: Consider extracting reaction update logic into separate functions.

The reaction management logic is comprehensive but could be more maintainable. The updateMessageReactions function handles multiple responsibilities.

Consider breaking down the logic into smaller, focused functions:

+const handleExistingReaction = (message: Message, userInfo: any, emoji: string) => {
+  const existingReaction = message.reactions?.find(
+    (r) => r.userId === userInfo.data?._id && r.emoji === emoji
+  );
+  if (existingReaction) {
+    return {
+      ...message,
+      reactions: message.reactions.filter(
+        (r) => !(r.userId === userInfo.data?._id && r.emoji === emoji)
+      ),
+    };
+  }
+  return null;
+};

+const handleOtherReaction = (message: Message, userInfo: any, emoji: string) => {
+  const hasOtherReaction = message.reactions?.find(
+    (r) => r.userId === userInfo.data?._id
+  );
+  if (hasOtherReaction) {
+    return {
+      ...message,
+      reactions: message.reactions.map((r) =>
+        r.userId === userInfo.data?._id ? { ...r, emoji } : r
+      ),
+    };
+  }
+  return null;
+};

 const updateMessageReactions = (message: Message) => {
   if (message._id !== messageId || message.type !== "message") {
     return message;
   }

-  const existingReaction = message.reactions?.find(
-    (r) => r.userId === userInfo.data?._id && r.emoji === emoji
-  );
-  if (existingReaction) {
-    return {
-      ...message,
-      reactions: message.reactions.filter(
-        (r) => !(r.userId === userInfo.data?._id && r.emoji === emoji)
-      ),
-    };
-  }
-
-  const hasOtherReaction = message.reactions?.find(
-    (r) => r.userId === userInfo.data?._id
-  );
-  if (hasOtherReaction) {
-    return {
-      ...message,
-      reactions: message.reactions.map((r) =>
-        r.userId === userInfo.data?._id ? { ...r, emoji } : r
-      ),
-    };
-  }
+  const existingReactionResult = handleExistingReaction(message, userInfo, emoji);
+  if (existingReactionResult) return existingReactionResult;
+
+  const otherReactionResult = handleOtherReaction(message, userInfo, emoji);
+  if (otherReactionResult) return otherReactionResult;

   return {
     ...message,
     reactions: [...(message.reactions || []), reaction],
   };
 };
src/components/message.tsx (3)

81-87: Simplify the reaction grouping logic.

The reduce operation with assignment in expression could be clearer.

Consider this cleaner approach:

-const reactionsByEmoji = reactions.reduce(
-  (acc, reaction) => {
-    (acc[reaction.emoji] = acc[reaction.emoji] ?? []).push(reaction);
-    return acc;
-  },
-  {} as Record<string, typeof reactions>,
-);
+const reactionsByEmoji = reactions.reduce((acc, reaction) => {
+  if (!acc[reaction.emoji]) {
+    acc[reaction.emoji] = [];
+  }
+  acc[reaction.emoji].push(reaction);
+  return acc;
+}, {} as Record<string, typeof reactions>);
🧰 Tools
🪛 Biome (1.9.4)

[error] 83-83: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


98-105: Simplify user lookup logic.

The nested ternary operators make the code hard to follow.

Consider this clearer approach:

-const user =
-  userInfos[0]?._id === reaction.userId
-    ? userInfos[0]
-    : Array.isArray(userInfos[1])
-      ? userInfos[1].find((u) => u._id === reaction.userId)
-      : userInfos[1];
+const findUser = (userId: Id<"users">) => {
+  if (userInfos[0]?._id === userId) return userInfos[0];
+  if (!Array.isArray(userInfos[1])) return userInfos[1];
+  return userInfos[1].find((u) => u._id === userId);
+};
+const user = findUser(reaction.userId);

114-154: Consider breaking down the Message component.

The component has many props and responsibilities, which could make it harder to maintain.

Consider splitting it into smaller components and grouping related props:

type MessageReactionProps = {
  reactions: Doc<"reactions">[];
  userInfos: UserInfos;
  onReaction: (messageId: Id<"messages">, emoji: string) => void;
};

type MessageActionsProps = {
  onEdit: (messageId: Id<"messages">) => void;
  onReply: (messageId: Id<"messages">) => void;
  onDelete: (messageId: Id<"messages">) => void;
};

type MessageProps = {
  message: Message;
  reactions: MessageReactionProps;
  actions: MessageActionsProps;
  ui: {
    selectedMessageId: Id<"messages"> | null;
    setSelectedMessageId: React.Dispatch<React.SetStateAction<Id<"messages"> | null>>;
    showEmojiPicker: boolean;
    setShowEmojiPicker: React.Dispatch<React.SetStateAction<boolean>>;
  };
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 10e346d and 3948f23.

📒 Files selected for processing (2)
  • src/app/(internal-sites)/chats/[chatId]/page.tsx (8 hunks)
  • src/components/message.tsx (5 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/message.tsx

[error] 83-83: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (1)
src/app/(internal-sites)/chats/[chatId]/page.tsx (1)

584-592: LGTM! Clean and efficient UI integration.

The EmojiPicker integration is well-implemented with proper prop spreading and conditional rendering.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3948f23 and d8bf6ae.

📒 Files selected for processing (1)
  • .gitignore (1 hunks)
🔇 Additional comments (1)
.gitignore (1)

52-52: Good security practice to ignore certificates!

Adding certificates to .gitignore is a good security practice to prevent sensitive certificate files from being accidentally committed to the repository.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (3)
src/components/message.tsx (3)

115-155: Extract props interface and consider using Context

The component's props structure could be improved:

  1. Extract the props interface to improve maintainability
  2. Consider using React Context for emoji picker and message selection state
type MessageProps = {
  message: Message;
  selectedMessageId: Id<"messages"> | null;
  setSelectedMessageId: React.Dispatch<React.SetStateAction<Id<"messages"> | null>>;
  setEditingMessageId: React.Dispatch<React.SetStateAction<Id<"messages"> | null>>;
  setReplyToMessageId: React.Dispatch<React.SetStateAction<Id<"messages"> | undefined>>;
  userInfos: UserInfos; // Reuse the extracted type
  setShowFullEmojiPicker: React.Dispatch<React.SetStateAction<boolean>>;
  refsFullEmojiPicker: {
    setReference: (node: ReferenceType | null) => void;
  };
  isInBottomHalf: boolean | null;
  setIsInBottomHalf: React.Dispatch<React.SetStateAction<boolean | null>>;
  reactToMessageHandler: (messageId: Id<"messages">, emoji: string) => void;
};

// Consider creating a MessageContext
const MessageContext = React.createContext<{
  selectedMessageId: Id<"messages"> | null;
  setSelectedMessageId: React.Dispatch<React.SetStateAction<Id<"messages"> | null>>;
  showEmojiPicker: boolean;
  setShowEmojiPicker: React.Dispatch<React.SetStateAction<boolean>>;
} | null>(null);

210-211: Extract mobile detection to a custom hook

The mobile detection logic could be moved to a reusable hook.

const useIsMobile = () => {
  const [isMobile, setIsMobile] = React.useState(false);
  
  React.useEffect(() => {
    const userAgent = navigator.userAgent;
    setIsMobile(/android|iPad|iPhone|iPod/i.test(userAgent));
  }, []);
  
  return isMobile;
};

Line range hint 1-854: Consider splitting the Message component

The Message component has grown quite large and handles multiple responsibilities. Consider splitting it into smaller, focused components:

  1. MessageContainer - Main wrapper handling layout
  2. MessageContent - Content rendering
  3. MessageReactions - Reaction handling
  4. MessageActions - Context menu actions

This would improve maintainability and make the code easier to test. Would you like assistance in planning this refactor?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d8bf6ae and 68c857b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • package.json (3 hunks)
  • src/components/message.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/message.tsx

[error] 84-84: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (1)
src/components/message.tsx (1)

525-557: Extract duplicate reaction rendering logic

The reaction rendering code is duplicated between sender and receiver message views. Extract it into a separate component.

Also applies to: 767-796

Comment on lines 340 to 438
<Trash2 />
<div className="ml-1">Delete</div>
</button>{" "}
<div className="flex border-t-2 border-secondary-foreground p-2 pr-8 text-secondary-foreground">
<Info />
<p className="ml-1">{sentInfo()}</p>
</div>
</div>
</div>
</div>,
chatContainerElement,
)
: null}
</div>
) : (
>
<span className="transition-transform hover:scale-125">
😂
</span>
</span>
<span
onMouseDown={() => reactToMessageHandler(message._id, "❤️")}
className={cn(
"flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 pt-1.5 hover:cursor-pointer dark:bg-primary",
{
"bg-muted-foreground dark:bg-card":
message.reactions.find(
(reaction) =>
reaction.emoji === "❤️" &&
reaction.userId === userInfos[0]?._id,
),
},
)}
>
<span className="transition-transform hover:scale-125">
❤️
</span>
</span>
<span
onMouseDown={() => reactToMessageHandler(message._id, "👍")}
className={cn(
"flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 pt-1.5 hover:cursor-pointer dark:bg-primary",
{
"bg-muted-foreground dark:bg-card":
message.reactions.find(
(reaction) =>
reaction.emoji === "👍" &&
reaction.userId === userInfos[0]?._id,
),
},
)}
>
<span className="transition-transform hover:scale-125">
👍
</span>
</span>
<span
onMouseDown={() => reactToMessageHandler(message._id, "👎")}
className={cn(
"flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 pt-1.5 hover:cursor-pointer dark:bg-primary",
{
"bg-muted-foreground dark:bg-card":
message.reactions.find(
(reaction) =>
reaction.emoji === "👎" &&
reaction.userId === userInfos[0]?._id,
),
},
)}
>
<span className="transition-transform hover:scale-125">
👎
</span>
</span>
<span
onMouseDown={() => reactToMessageHandler(message._id, "😮")}
className={cn(
"flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 pt-1.5 hover:cursor-pointer dark:bg-primary",
{
"bg-muted-foreground dark:bg-card":
message.reactions.find(
(reaction) =>
reaction.emoji === "😮" &&
reaction.userId === userInfos[0]?._id,
),
},
)}
>
<span className="transition-transform hover:scale-125">
😮
</span>
</span>
<span
onMouseDown={() =>
setShowFullEmojiPicker((prevValue) => !prevValue)
}
className="flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 hover:cursor-pointer dark:bg-primary"
>
<Plus className="transition-transform hover:scale-125" />
</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add accessibility attributes to emoji buttons

The emoji reaction buttons need proper accessibility attributes.

 <span
   onMouseDown={() => reactToMessageHandler(message._id, "😂")}
+  role="button"
+  aria-label="React with laughing emoji"
+  tabIndex={0}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      reactToMessageHandler(message._id, "😂");
+    }
+  }}
   className={cn(
     "flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 pt-1.5 hover:cursor-pointer dark:bg-primary",
     {
       "bg-muted-foreground dark:bg-card":
         message.reactions.find(
           (reaction) =>
             reaction.emoji === "😂" &&
             reaction.userId === userInfos[0]?._id,
         ),
     },
   )}
 >

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (1)
src/components/message.tsx (1)

210-211: Consider using a more robust mobile detection method

Using user agent strings for mobile detection can be unreliable due to spoofing and constant changes in user agent patterns.

Consider using the window.matchMedia API for a more reliable solution:

- const userAgent = navigator.userAgent;
- const isMobile = /android|iPad|iPhone|iPod/i.test(userAgent);
+ const isMobile = window.matchMedia('(max-width: 768px)').matches ||
+   ('ontouchstart' in window) ||
+   (navigator.maxTouchPoints > 0);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 68c857b and 731f004.

📒 Files selected for processing (1)
  • src/components/message.tsx (5 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/message.tsx

[error] 84-84: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (4)
src/components/message.tsx (4)

Line range hint 1-854: Implementation successfully adds reaction functionality

The implementation successfully meets the PR objectives by adding emoji reactions to messages. The code is well-structured with good separation of concerns. The suggested improvements focus on:

  1. Type safety and code reusability
  2. Accessibility support
  3. Mobile detection robustness
  4. Eliminating code duplication

Please consider implementing the suggested improvements, but the core functionality is working as expected.


308-317: ⚠️ Potential issue

Fix undefined event variable in onLongPress handler

The onLongPress function references an undefined e variable when calling checkClickPosition(e).

Apply this fix:

- const onLongPress = () => {
+ const onLongPress = (e: React.MouseEvent | React.TouchEvent) => {
    if (!isMobile) return;
    if (
      (message.type === "message" && message.deleted) ||
      message.type !== "message"
    )
      return;
    checkClickPosition(e);
    setSelectedMessageId(message._id);
  };

Likely invalid or redundant comment.


66-114: 🛠️ Refactor suggestion

Improve type safety and readability of the ReactionDetails component

The component has several areas that need improvement:

  1. The reduce operation uses a confusing assignment in expression
  2. The user lookup logic is complex and duplicated
  3. Types can be better defined

Apply these improvements:

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

+ const findUserById = (userId: string, userInfos: UserInfos) => {
+   if (userInfos[0]?._id === userId) return userInfos[0];
+   return Array.isArray(userInfos[1])
+     ? userInfos[1].find((u) => u._id === userId)
+     : userInfos[1];
+ };

 const reactionsByEmoji = reactions.reduce((acc, reaction) => {
-  (acc[reaction.emoji] = acc[reaction.emoji] ?? []).push(reaction);
+  if (!acc[reaction.emoji]) {
+    acc[reaction.emoji] = [];
+  }
+  acc[reaction.emoji].push(reaction);
   return acc;
 }, {} as Record<string, typeof reactions>);

 // In the render section:
-{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];
+{reactions.map((reaction) => {
+  const user = findUserById(reaction.userId, userInfos);
   return user?.username;
}).join(", ")}

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 84-84: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


340-438: 🛠️ Refactor suggestion

Add accessibility support to emoji reaction buttons

The emoji reaction buttons lack proper accessibility attributes and keyboard navigation support.

Add ARIA labels and keyboard support:

 <span
   onMouseDown={() => reactToMessageHandler(message._id, "😂")}
+  role="button"
+  aria-label="React with laughing emoji"
+  tabIndex={0}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      reactToMessageHandler(message._id, "😂");
+    }
+  }}
   className={cn(/*...*/)}
 >

Likely invalid or redundant comment.

Comment on lines +515 to +565
{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 },
)}
>
{message.reactions
.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="flex items-center justify-center rounded-full bg-primary/20 text-sm"
>
<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>
</div>
</div>,
chatContainerElement,
)
))}
</PopoverTrigger>
<PopoverContent>
<ReactionDetails
reactions={message.reactions}
userInfos={userInfos}
/>
</PopoverContent>
</Popover>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Eliminate code duplication in reaction UI

The reaction counting and display logic is duplicated between sender and receiver message views.

Extract the common logic into a reusable component:

type MessageReactionsProps = {
  reactions: Doc<"reactions">[];
  userInfos: UserInfos;
  alignment: 'left' | 'right';
};

const MessageReactions: React.FC<MessageReactionsProps> = ({ reactions, userInfos, alignment }) => {
  const groupedReactions = reactions.reduce((acc, reaction) => {
    if (!acc[reaction.emoji]) {
      acc[reaction.emoji] = { count: 0, emoji: reaction.emoji };
    }
    acc[reaction.emoji].count++;
    return acc;
  }, {} as Record<string, { count: number; emoji: string }>);

  return (
    <Popover>
      <PopoverTrigger className={cn(
        "absolute flex items-center justify-center gap-1 rounded-full bg-secondary px-1",
        {
          "left-0 -translate-x-[0%]": alignment === 'left',
          "right-0 translate-x-[0%]": alignment === 'right'
        }
      )}>
        {Object.values(groupedReactions).map((reaction, index) => (
          <ReactionCounter key={index} {...reaction} />
        ))}
      </PopoverTrigger>
      <PopoverContent>
        <ReactionDetails reactions={reactions} userInfos={userInfos} />
      </PopoverContent>
    </Popover>
  );
};

Then use it in the Message component:

-{message.reactions && message.reactions.length > 0 && (
-  <Popover>
-    <PopoverTrigger className="absolute right-0 flex -translate-x-[0%] items-center justify-center gap-1 rounded-full bg-secondary px-1">
-      {/* ... reaction counting logic ... */}
-    </PopoverTrigger>
-    <PopoverContent>
-      <ReactionDetails reactions={message.reactions} userInfos={userInfos} />
-    </PopoverContent>
-  </Popover>
-)}
+{message.reactions && message.reactions.length > 0 && (
+  <MessageReactions
+    reactions={message.reactions}
+    userInfos={userInfos}
+    alignment={message.from._id === userInfos[0]?._id ? 'right' : 'left'}
+  />
+)}

Also applies to: 757-805

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (2)
src/components/message.tsx (2)

313-322: Improve long press handler type safety

The long press handler's event type could be more specific and the function could be memoized.

Apply these improvements:

-const onLongPress = (e: MouseEvent | TouchEvent) => {
+const onLongPress = React.useCallback((e: React.TouchEvent | React.MouseEvent) => {
   if (!isMobile) return;
   if (
     (message.type === "message" && message.deleted) ||
     message.type !== "message"
   )
     return;
   checkClickPosition(e);
   setSelectedMessageId(message._id);
-};
+}, [isMobile, message, checkClickPosition, setSelectedMessageId]);

215-216: Extract mobile detection to a custom hook

The mobile detection logic could be moved to a reusable hook.

Create a new hook:

const useIsMobile = () => {
  const [isMobile, setIsMobile] = React.useState(false);
  
  React.useEffect(() => {
    const userAgent = navigator.userAgent;
    setIsMobile(/android|iPad|iPhone|iPod/i.test(userAgent));
  }, []);
  
  return isMobile;
};

Then use it in the component:

-const userAgent = navigator.userAgent;
-const isMobile = /android|iPad|iPhone|iPod/i.test(userAgent);
+const isMobile = useIsMobile();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 731f004 and 8680de0.

📒 Files selected for processing (1)
  • src/components/message.tsx (6 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/message.tsx

[error] 84-84: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (2)
src/components/message.tsx (2)

345-443: 🛠️ Refactor suggestion

Add accessibility attributes to emoji buttons

The emoji reaction buttons need proper accessibility attributes for better screen reader support.

Apply these improvements to each emoji button:

 <span
   onMouseDown={() => reactToMessageHandler(message._id, "😂")}
+  role="button"
+  aria-label="React with laughing emoji"
+  tabIndex={0}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      reactToMessageHandler(message._id, "😂");
+    }
+  }}
   className={cn(
     "flex aspect-square h-10 items-center justify-center rounded-full bg-card p-1 pt-1.5 hover:cursor-pointer dark:bg-primary",
     {
       "bg-muted-foreground dark:bg-card":
         message.reactions.find(
           (reaction) =>
             reaction.emoji === "😂" &&
             reaction.userId === userInfos[0]?._id,
         ),
     },
   )}
 >

Likely invalid or redundant comment.


66-114: 🛠️ Refactor suggestion

Simplify reaction grouping logic and improve type safety

The component has several areas that could be improved:

  1. The reaction grouping logic uses assignment in expression which is error-prone
  2. User lookup logic is complex and duplicated
  3. Missing error handling for undefined users

Apply these improvements:

 const ReactionDetails = ({
   reactions,
   userInfos,
 }: {
   reactions: Doc<"reactions">[];
   userInfos: [
     FunctionReturnType<typeof api.users.getUserData> | undefined,
     | undefined
     | NonNullable<FunctionReturnType<typeof api.chats.getChatInfoFromId>>["otherUser"]
   ];
 }) => {
+  const findUser = (userId: string) => 
+    userInfos[0]?._id === userId
+      ? userInfos[0]
+      : Array.isArray(userInfos[1])
+        ? userInfos[1].find((u) => u._id === userId)
+        : userInfos[1];
+
   const reactionsByEmoji = reactions.reduce(
     (acc, reaction) => {
-      (acc[reaction.emoji] = acc[reaction.emoji] ?? []).push(reaction);
+      if (!acc[reaction.emoji]) {
+        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;
+                  const user = findUser(reaction.userId);
+                  return user?.username ?? 'Unknown User';
                 })
                 .join(", ")}
             </div>
           </div>
         </div>
       ))}
     </div>
   );
 };

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 84-84: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

Comment on lines +449 to +857
</div>
)}
</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 },
)}
>
{message.reactions
.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="flex items-center justify-center rounded-full bg-primary/20 text-sm"
>
<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>
))}
</PopoverTrigger>
<PopoverContent>
<ReactionDetails
reactions={message.reactions}
userInfos={userInfos}
/>
</PopoverContent>
</Popover>
)}

{chatContainerElement &&
message._id == selectedMessageId &&
message.type == "message"
? // The reason for the creation of the portal is that we need the portal at a point where it is over EVERYTHING even the input etc.
createPortal(
<div
ref={refsContextModal.setFloating}
style={floatingStylesContextModal}
className="z-50 py-3 opacity-100"
>
<div className="rounded-sm border-2 border-secondary-foreground">
<div className="rounded-sm bg-secondary">
<div
onClick={() => {
void navigator.clipboard.writeText(message.content);
setSelectedMessageId(null);
setShowFullEmojiPicker(false);
toast.success("Copied to clipboard");
}}
className="flex cursor-pointer p-2"
>
<CopyCheck />
<p className="ml-1">Copy</p>
</div>
<button
onClick={() => replyToMessageHandler(message._id)}
className="flex w-full cursor-pointer border-y-2 border-secondary-foreground p-2 pr-8"
>
<Reply />
<p className="ml-1">Reply</p>
</button>
<div className="flex w-full cursor-pointer border-b-2 border-secondary-foreground p-2 pr-8">
<Forward />
<p className="ml-1">Forward</p>
</div>
<div className="flex p-2 pr-8 text-secondary-foreground">
<Info />
<p className="ml-1">{sentInfo()}</p>
</div>
</div>
</div>
</div>,
chatContainerElement,
)
: null}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract common message rendering logic

The message rendering logic is duplicated between sender and receiver views. This makes maintenance harder and increases the chance of bugs.

Create a shared component for the message content:

type MessageContentProps = {
  message: Message;
  userInfos: UserInfos;
  chatInfo: ChatInfo;
  getTimeRemaining: () => string;
};

const MessageContent: React.FC<MessageContentProps> = ({
  message,
  userInfos,
  chatInfo,
  getTimeRemaining,
}) => {
  if (message.type === "message" && message.deleted) {
    return (
      <div className="flex font-medium">
        <Ban />
        <p className="ml-2.5">This message was deleted</p>
      </div>
    );
  }

  if (message.type !== "message") {
    return <PendingRequestContent message={message} chatInfo={chatInfo} getTimeRemaining={getTimeRemaining} />;
  }

  return (
    <div className="select-none lg:select-auto">
      <div>{message.content}</div>
    </div>
  );
};

Then use it in both sender and receiver views:

-{message.type === "message" && message.deleted ? (
-  <div className="flex font-medium">
-    <Ban />
-    <p className="ml-2.5">This message was deleted</p>
-  </div>
-) : (
-  // ... complex conditional rendering
-)}
+<MessageContent
+  message={message}
+  userInfos={userInfos}
+  chatInfo={chatInfo}
+  getTimeRemaining={getTimeRemaining}
+/>

@FleetAdmiralJakob FleetAdmiralJakob merged commit c14fa60 into main Dec 2, 2024
6 of 8 checks passed
@FleetAdmiralJakob FleetAdmiralJakob deleted the reactions branch December 2, 2024 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

React Functionality
1 participant