-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: add reactions #765
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Caution Review failedThe pull request is closed. WalkthroughThe changes introduce a new feature allowing users to react to messages with emojis. A mutation function Changes
Assessment against linked issues
Possibly related PRs
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
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? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
New dependencies detected. Learn more about Socket for GitHub ↗︎
|
There was a problem hiding this 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 statementThe
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 inReactionDetails
componentThe redundant
if
check can be removed because the nullish coalescing operator??
already initializes the array whenacc[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 valueThe 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
⛔ 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
There was a problem hiding this 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 propsThe
EmojiPicker
component could benefit from:
- Error boundaries to handle potential runtime errors from the third-party picker
- 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 componentThe 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 renderingThe 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
📒 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.
There was a problem hiding this 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
📒 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.
There was a problem hiding this 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
📒 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.
There was a problem hiding this 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 ContextThe component's props structure could be improved:
- Extract the props interface to improve maintainability
- 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 hookThe 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 componentThe Message component has grown quite large and handles multiple responsibilities. Consider splitting it into smaller, focused components:
- MessageContainer - Main wrapper handling layout
- MessageContent - Content rendering
- MessageReactions - Reaction handling
- 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
⛔ 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
src/components/message.tsx
Outdated
<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> |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 methodUsing 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
📒 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:
- Type safety and code reusability
- Accessibility support
- Mobile detection robustness
- Eliminating code duplication
Please consider implementing the suggested improvements, but the core functionality is working as expected.
308-317
:
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:
- The reduce operation uses a confusing assignment in expression
- The user lookup logic is complex and duplicated
- 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.
{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> |
There was a problem hiding this comment.
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
There was a problem hiding this 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 safetyThe 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 hookThe 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
📒 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:
- The reaction grouping logic uses assignment in expression which is error-prone
- User lookup logic is complex and duplicated
- 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)
</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> |
There was a problem hiding this comment.
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}
+/>
closes #672
Summary by CodeRabbit
Release Notes
New Features
EmojiPicker
component has been added to select emojis easily.ReactionDetails
component.Bug Fixes
Style
Chores
.gitignore
to excludecertificates
from tracking.