-
Notifications
You must be signed in to change notification settings - Fork 898
fixed #348 #396
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
base: main
Are you sure you want to change the base?
fixed #348 #396
Conversation
@Saksham1387 is attempting to deploy a commit to the Inbox Zero Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThe pull request enhances the Changes
Sequence Diagram(s)sequenceDiagram
participant E as EmailListItem
participant L as useLabels Hook
participant B as Badge Component
E->>L: Retrieve userLabels
Note over E: Memoize labelsToDisplay using labelIds & userLabels
E->>B: For each label in labelsToDisplay, render Badge
Assessment against linked issues
Possibly related PRs
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
apps/web/components/email-list/EmailListItem.tsxOops! Something went wrong! :( ESLint: 9.23.0 ESLint couldn't find an eslint.config.(js|mjs|cjs) file. From ESLint v9.0.0, the default configuration file is now eslint.config.js. https://eslint.org/docs/latest/use/configure/migration-guide If you still have problems after following the migration guide, please stop by ✨ Finishing Touches
🪧 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 (
|
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: 0
🧹 Nitpick comments (3)
apps/web/components/email-list/EmailListItem.tsx (3)
156-168
: Consider extracting duplicated label rendering codeThe label rendering code is duplicated in two locations. Consider extracting this into a reusable component or function to follow the DRY principle.
+ // Add this near the top of the file or in a separate component file + const EmailLabels = ({ labels }: { labels: Array<{ id: string, name: string }> }) => { + if (!labels || labels.length === 0) return null; + + return ( + <span className="ml-2 inline-flex flex-wrap items-center gap-1"> + {labels.map((label) => ( + <Badge + variant="secondary" + key={label.id} + className="text-xs" + > + {label.name} + </Badge> + ))} + </span> + ); + }; // Then replace the duplicated sections with: - {labelsToDisplay && labelsToDisplay.length > 0 && ( - <span className="ml-2 inline-flex flex-wrap items-center gap-1"> - {labelsToDisplay.map((label) => ( - <Badge - variant="secondary" - key={label.id} - className="text-xs" - > - {label.name} - </Badge> - ))} - </span> - )} + <EmailLabels labels={labelsToDisplay} />Also applies to: 226-238
63-65
: Consider preserving label color informationIf the Gmail API provides color information for labels, consider incorporating it into the Badge component to match Gmail's visual representation.
- return { id, name: label.name }; + return { + id, + name: label.name, + color: label.color || undefined + }; // Then in the Badge component: <Badge variant="secondary" key={label.id} className="text-xs" + style={label.color ? { backgroundColor: label.color.backgroundColor, color: label.color.textColor } : undefined} > {label.name} </Badge>
61-67
: Consider handling empty label arrays more efficientlyThe current implementation will return undefined if labelIds is undefined, but an empty array would be more consistent with the expected return type.
- return labelIds + return labelIds || []This ensures that when labelIds is undefined, an empty array is returned rather than undefined, which is more consistent with the expected array type and can prevent potential issues when checking length.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/components/email-list/EmailListItem.tsx
(4 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
apps/web/components/email-list/EmailListItem.tsx (1)
apps/web/utils/types.ts (1)
isDefined
(8-10)
🔇 Additional comments (3)
apps/web/components/email-list/EmailListItem.tsx (3)
24-26
: Good additions to support label feature integrationThe new imports are well-chosen to support the label display functionality:
- Badge component for visual representation
- isDefined utility for safe filtering
- useLabels hook for accessing the label data
51-51
: Appropriate hook usageThe useLabels hook is correctly used to retrieve user-defined labels.
59-68
: Well-implemented memoization for label processingThis implementation correctly:
- Uses useMemo to optimize performance
- Handles the mapping of label IDs to their display names
- Filters out undefined values
- Sets proper dependencies to trigger recalculation when needed
const labelIds = lastMessage?.labelIds; | ||
return labelIds | ||
?.map((id) => { | ||
const label = userLabels[Number(id)]; |
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.
why do we convert to number?
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.
otherwise it will be of type any and typescript complains
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.
doesn't make sense. something hacky going on here.
{labelsToDisplay.map((label) => ( | ||
<Badge | ||
variant="secondary" | ||
key={label.id} | ||
className="text-xs" | ||
> | ||
{label.name} | ||
</Badge> | ||
))} |
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.
this appears twice. we could make this a small component at the bottom of this file
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.
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
Plan: Pro
📒 Files selected for processing (1)
apps/web/components/email-list/EmailListItem.tsx
(5 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
apps/web/components/email-list/EmailListItem.tsx (3)
apps/web/hooks/useLabels.ts (1)
useLabels
(37-52)apps/web/utils/types.ts (1)
isDefined
(8-10)apps/web/components/Badge.tsx (1)
Badge
(30-47)
🔇 Additional comments (4)
apps/web/components/email-list/EmailListItem.tsx (4)
24-26
: Appropriate imports added for label functionality.The new imports are well-chosen for implementing the label display feature. The
Badge
component will style the labels,isDefined
helps with type-safe filtering, anduseLabels
provides access to the user's labels.
51-51
: Good use of the useLabels hook.The hook is correctly used to retrieve user labels for display in the component.
156-158
: The label display logic appears in two locations.The labels are conditionally rendered in both the regular view and split view sections of the component. This duplication is necessary given the different UI layouts, and you've correctly extracted the rendering logic into a separate component to avoid code duplication.
Also applies to: 216-218
241-261
: Well-structured label display component.The
LabelsDisplay
component is well-designed with proper type definitions, conditional rendering for empty states, and appropriate styling. The implementation correctly maps through the labels and displays them as badges.
const labelsToDisplay = useMemo(() => { | ||
const labelIds = lastMessage?.labelIds; | ||
return labelIds | ||
?.map((id) => { | ||
const label = userLabels[Number(id)]; | ||
if (!label) return null; | ||
return { id, name: label.name }; | ||
}) | ||
.filter(isDefined); | ||
}, [lastMessage?.labelIds, userLabels]); |
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.
Fix the label lookup logic.
The current implementation treats userLabels
as an object with numeric keys, but according to the useLabels
hook implementation, it returns an array. You should use array methods like find
to locate labels by ID.
const labelsToDisplay = useMemo(() => {
const labelIds = lastMessage?.labelIds;
return labelIds
?.map((id) => {
- const label = userLabels[Number(id)];
+ const label = userLabels.find(label => label.id === id);
if (!label) return null;
return { id, name: label.name };
})
.filter(isDefined);
}, [lastMessage?.labelIds, userLabels]);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const labelsToDisplay = useMemo(() => { | |
const labelIds = lastMessage?.labelIds; | |
return labelIds | |
?.map((id) => { | |
const label = userLabels[Number(id)]; | |
if (!label) return null; | |
return { id, name: label.name }; | |
}) | |
.filter(isDefined); | |
}, [lastMessage?.labelIds, userLabels]); | |
const labelsToDisplay = useMemo(() => { | |
const labelIds = lastMessage?.labelIds; | |
return labelIds | |
?.map((id) => { | |
const label = userLabels.find(label => label.id === id); | |
if (!label) return null; | |
return { id, name: label.name }; | |
}) | |
.filter(isDefined); | |
}, [lastMessage?.labelIds, userLabels]); |
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.
see this
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.
🧩 Analysis chain
@elie222 I see the CodeRabbit suggestion about the label lookup logic. Let me investigate the actual implementation of the useLabels
hook to verify whether the current approach or the suggested change is correct.
🏁 Script executed:
#!/bin/bash
# Find the useLabels hook implementation
fd -e tsx -e ts -e js useLabels
Length of output: 61
🏁 Script executed:
#!/bin/bash
# Search for the useLabels hook definition
ast-grep --pattern 'export function useLabels() {
$$$
}'
Length of output: 878
🏁 Script executed:
#!/bin/bash
# Alternative search for useLabels hook
rg "useLabels" -A 20 -B 5
Length of output: 28833
@elie222 You're right to point this out! After examining the useLabels
hook implementation, I can confirm that CodeRabbit's suggestion is correct.
The current implementation in the diff is incorrect because:
-
userLabels
is an array, not an object: TheuseLabels()
hook returnsuserLabels
as an array:data?.labels?.filter(isUserLabel).sort(sortLabels) || []
-
Label IDs are strings, not numeric indices: Gmail label IDs are strings like
"UNREAD"
,"INBOX"
, user-defined label IDs, etc. Converting them to numbers withNumber(id)
and using them as array indices doesn't make sense. -
Incorrect lookup method:
userLabels[Number(id)]
tries to access array elements by numeric index, but we need to find labels by theirid
property.
CodeRabbit's suggested fix is correct:
const label = userLabels.find(label => label.id === id);
This properly searches through the array to find a label object whose id
property matches the label ID from the email message.
The current code would likely return undefined
for most labels, causing the label badges not to display properly.
🏁 Script executed:
#!/bin/bash
# Let's also examine the file content to see the complete useLabels hook implementation
cat apps/web/hooks/useLabels.ts
Length of output: 2124
Perfect! Now I can see the complete picture. The useLabels
hook implementation confirms that CodeRabbit's suggestion is absolutely correct.
Here's the definitive analysis:
The Problem
The current code:
const label = userLabels[Number(id)];
This is wrong because:
userLabels
is an array ofUserLabel
objects, not an object with numeric keys- Label IDs are strings (like
"UNREAD"
,"INBOX"
, or custom label IDs like"Label_123"
) - Array indices have no relation to label IDs - the array is just a filtered and sorted list of user labels
The Correct Solution
CodeRabbit's suggestion is the proper way to find labels:
const label = userLabels.find(label => label.id === id);
This correctly:
- Searches through the
userLabels
array - Finds the label object whose
id
property matches the email's label ID - Returns the matching
UserLabel
object orundefined
if not found
Impact
The current incorrect implementation would cause:
- Label badges not displaying for most emails
- Potential runtime errors or unexpected behavior
- Labels being silently ignored rather than shown to users
CodeRabbit's suggestion should definitely be applied to fix the label display functionality in the email list.
🧬 Code Graph Analysis Results
Relevant snippets for understanding apps/web/components/email-list/EmailListItem.tsx
File: apps/web/utils/types.ts
(lines 8-10)
export function isDefined<T>(value: T | undefined | null): value is T {
return value !== undefined && value !== null;
}
- Utility function used in
EmailListItem
to filter out undefined or null values from arrays, ensuring type safety.
File: apps/web/components/email-list/types.ts
(lines 5-11)
export type Thread = {
id: FullThread["id"];
messages: FullThread["messages"];
snippet: FullThread["snippet"];
plan: FullThread["plan"];
category: FullThread["category"];
};
- Defines the
Thread
type used as a prop inEmailListItem
. It includes email thread metadata such as messages, snippet, plan, and category.
File: apps/web/utils/email.ts
(lines 6-13 and 74-81)
export function extractNameFromEmail(email: string) {
if (!email) return "";
const firstPart = email.split("<")[0]?.trim();
if (firstPart) return firstPart;
const secondPart = email.split("<")?.[1]?.trim();
if (secondPart) return secondPart.split(">")[0];
return email;
}
export function participant(
message: { headers: Pick<ParsedMessage["headers"], "from" | "to"> },
userEmail: string,
) {
if (!userEmail) return message.headers.from;
if (message.headers.from.includes(userEmail)) return message.headers.to;
return message.headers.from;
}
extractNameFromEmail
extracts a display name from an email string.participant
determines the other participant in the email thread relative to the user's email address.- Both are used in
EmailListItem
to display sender/recipient names.
File: apps/web/utils/gmail/decode.ts
(implied usage)
- The
decodeSnippet
function is imported and used to decode email snippet text for display. - (No snippet provided, but relevant as it processes the snippet content.)
File: apps/web/utils/parse/parseHtml.client.ts
(implied usage)
- The
findCtaLink
function is imported and used to find a call-to-action link in the email's HTML content. - (No snippet provided, but relevant for rendering CTA buttons.)
File: apps/web/hooks/useLabels.ts
(lines 37-52)
export function useLabels() {
const { data, isLoading, error, mutate } =
useSWR<LabelsResponse>("/api/google/labels");
const userLabels = useMemo(
() => data?.labels?.filter(isUserLabel).sort(sortLabels) || [],
[data?.labels],
);
return {
userLabels,
isLoading,
error,
mutate,
};
}
- Custom hook used in
EmailListItem
to fetch and provide user labels for emails. - Labels are used to display badges next to email subjects.
File: apps/web/utils/date.ts
(lines 46-53)
export function internalDateToDate(internalDate?: string | null): Date {
if (!internalDate) return new Date();
const date = new Date(+internalDate);
if (Number.isNaN(date.getTime())) return new Date();
return date;
}
- Converts internal date string (likely from Gmail API) to JavaScript
Date
object. - Used in
EmailListItem
to display the email date.
File: apps/web/components/email-list/EmailDate.tsx
(lines 3-9)
export function EmailDate(props: { date: Date }) {
return (
<div className="flex-shrink-0 text-sm font-medium leading-5 text-muted-foreground">
{formatShortDate(props.date)}
</div>
);
}
- Component used in
EmailListItem
to render the formatted date of the email.
File: apps/web/components/ActionButtons.tsx
(lines 14-85)
export function ActionButtons({
threadId,
onArchive,
onPlanAiAction,
isPlanning,
refetch,
shadow,
}: {
threadId: string;
isPlanning: boolean;
shadow?: boolean;
onPlanAiAction: () => void;
onArchive: () => void;
refetch: (threadId?: string) => void;
}) {
const session = useSession();
const email = session.data?.user.email;
const openInGmail = useCallback(() => {
// open in gmail
const url = getGmailUrl(threadId, email);
window.open(url, "_blank");
}, [threadId, email]);
const [isTrashing, setIsTrashing] = useState(false);
// TODO lift this up to the parent component to be consistent / to support bulk trash
// TODO show loading toast
const onTrash = useCallback(async () => {
setIsTrashing(true);
await onTrashThread(threadId);
refetch(threadId);
setIsTrashing(false);
}, [threadId, refetch]);
const buttons = useMemo(
() => [
{
tooltip: "Open in Gmail",
onClick: openInGmail,
icon: <ExternalLinkIcon className="size-4" aria-hidden="true" />,
},
{
tooltip: "Process with assistant",
onClick: onPlanAiAction,
icon: isPlanning ? (
<LoadingMiniSpinner />
) : (
<SparklesIcon className="size-4" aria-hidden="true" />
),
},
{
tooltip: "Archive",
onClick: onArchive,
icon: <ArchiveIcon className="size-4" aria-hidden="true" />,
},
// may remove later
{
tooltip: "Delete",
onClick: onTrash,
icon: isTrashing ? (
<LoadingMiniSpinner />
) : (
<Trash2Icon className="size-4" aria-hidden="true" />
),
},
],
[onTrash, isTrashing, onArchive, onPlanAiAction, isPlanning, openInGmail],
);
return <ButtonGroup buttons={buttons} shadow={shadow} />;
}
- Component rendering action buttons (Open in Gmail, Process with assistant, Archive, Delete) for each email thread.
- Used inside
EmailListItem
for the right-side action buttons.
File: apps/web/components/ui/badge.tsx
(lines 30-47)
export const Badge = forwardRef(
(
props: { children: React.ReactNode; color: Color; className?: string },
ref: ForwardedRef<HTMLSpanElement | null>,
) => {
const { color, className, ...rest } = props;
return (
<span
ref={ref}
{...rest}
className={cn(badgeVariants({ color, className }))}
>
{props.children}
</span>
);
},
);
- UI component used to render badges, such as labels in
LabelsDisplay
insideEmailListItem
.
File: apps/web/components/ui/button.tsx
(line 99)
Button
- The
Button
component is imported and used inEmailListItem
to render clickable buttons, including the CTA button.
Summary
The above snippets provide context on:
- Types and utilities for email threads and labels.
- Helper functions for extracting participant names and decoding snippets.
- UI components for badges, buttons, dates, and action buttons.
- Hooks for fetching user labels.
- Date conversion utilities.
These are all directly used or referenced in EmailListItem.tsx
and are relevant to understanding its implementation and behavior.
Fixes #348
Summary by CodeRabbit