diff --git a/apps/mail/app/(routes)/settings/labels/page.tsx b/apps/mail/app/(routes)/settings/labels/page.tsx index f69b7b236..e9b16487d 100644 --- a/apps/mail/app/(routes)/settings/labels/page.tsx +++ b/apps/mail/app/(routes)/settings/labels/page.tsx @@ -15,6 +15,7 @@ import { } from '@/components/ui/form'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { SettingsCard } from '@/components/settings/settings-card'; +import { LabelDialog } from '@/components/labels/label-dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { CurvedArrow } from '@/components/icons/icons'; import { Separator } from '@/components/ui/separator'; @@ -42,21 +43,14 @@ export default function LabelsPage() { const { data: labels, isLoading, error, refetch } = useLabels(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingLabel, setEditingLabel] = useState(null); - const form = useForm({ - defaultValues: { - name: '', - color: { backgroundColor: '#E2E2E2', textColor: '#000000' }, - }, - }); + const trpc = useTRPC(); const { mutateAsync: createLabel } = useMutation(trpc.labels.create.mutationOptions()); const { mutateAsync: updateLabel } = useMutation(trpc.labels.update.mutationOptions()); const { mutateAsync: deleteLabel } = useMutation(trpc.labels.delete.mutationOptions()); - const formColor = form.watch('color'); - - const onSubmit = async (data: LabelType) => { - toast.promise( + const handleSubmit = async (data: LabelType) => { + await toast.promise( editingLabel ? updateLabel({ id: editingLabel.id!, name: data.name, color: data.color }) : createLabel({ color: data.color, name: data.name }), @@ -64,10 +58,6 @@ export default function LabelsPage() { loading: 'Saving label...', success: 'Label saved successfully', error: 'Failed to save label', - finally: async () => { - await refetch(); - handleClose(); - }, }, ); }; @@ -83,135 +73,33 @@ export default function LabelsPage() { }); }; - const handleEdit = async (label: LabelType) => { + const handleEdit = (label: LabelType) => { setEditingLabel(label); - form.reset({ - name: label.name, - color: label.color, - }); setIsDialogOpen(true); }; - const handleClose = () => { - setIsDialogOpen(false); - setEditingLabel(null); - form.reset({ - name: '', - color: { backgroundColor: '#E2E2E2', textColor: '#000000' }, - }); - }; - return (
-
- - - - -
- - - {editingLabel ? 'Edit Label' : 'Create New Label'} - -
-
- ( - - Label Name - - - - - - )} - /> -
-
- -
-
- {[ - // Row 1 - Grayscale - '#000000', - '#434343', - '#666666', - '#999999', - '#cccccc', - '#ffffff', - // Row 2 - Warm colors - '#fb4c2f', - '#ffad47', - '#fad165', - '#ff7537', - '#cc3a21', - '#8a1c0a', - // Row 3 - Cool colors - '#16a766', - '#43d692', - '#4a86e8', - '#285bac', - '#3c78d8', - '#0d3472', - // Row 4 - Purple tones - '#a479e2', - '#b99aff', - '#653e9b', - '#3d188e', - '#f691b3', - '#994a64', - // Row 5 - Pastels - '#f6c5be', - '#ffe6c7', - '#c6f3de', - '#c9daf8', - ].map((color) => ( -
-
-
-
-
- - -
-
-
-
-
- + setEditingLabel(null)}> + + Create Label + + } + editingLabel={editingLabel} + open={isDialogOpen} + onOpenChange={(open) => { + setIsDialogOpen(open); + if (!open) setEditingLabel(null); + }} + onSubmit={handleSubmit} + onSuccess={refetch} + /> } >
diff --git a/apps/mail/components/labels/label-dialog.tsx b/apps/mail/components/labels/label-dialog.tsx new file mode 100644 index 000000000..3ecad0cd3 --- /dev/null +++ b/apps/mail/components/labels/label-dialog.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { CurvedArrow } from '@/components/icons/icons'; +import { LABEL_COLORS } from '@/lib/label-colors'; +import type { Label as LabelType } from '@/types'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { Command } from 'lucide-react'; + +interface LabelDialogProps { + trigger?: React.ReactNode; + onSuccess?: () => void; + editingLabel?: LabelType | null; + open?: boolean; + onOpenChange?: (open: boolean) => void; + onSubmit: (data: LabelType) => Promise; +} + +export function LabelDialog({ + trigger, + onSuccess, + editingLabel, + open, + onOpenChange, + onSubmit, +}: LabelDialogProps) { + const [isOpen, setIsOpen] = useState(false); + const isControlled = open !== undefined; + const dialogOpen = isControlled ? open : isOpen; + const setDialogOpen = isControlled ? onOpenChange! : setIsOpen; + + const form = useForm({ + defaultValues: { + name: '', + color: { backgroundColor: '#E2E2E2', textColor: '#000000' }, + }, + }); + + const formColor = form.watch('color'); + + // Reset form when editingLabel changes or dialog opens + useEffect(() => { + if (dialogOpen) { + if (editingLabel) { + form.reset({ + name: editingLabel.name, + color: editingLabel.color || { backgroundColor: '#E2E2E2', textColor: '#000000' }, + }); + } else { + form.reset({ + name: '', + color: { backgroundColor: '#E2E2E2', textColor: '#000000' }, + }); + } + } + }, [dialogOpen, editingLabel, form]); + + const handleSubmit = async (data: LabelType) => { + await onSubmit(data); + handleClose(); + onSuccess?.(); + }; + + const handleClose = () => { + setDialogOpen(false); + form.reset({ + name: '', + color: { backgroundColor: '#E2E2E2', textColor: '#000000' }, + }); + }; + + return ( + + {trigger && {trigger}} +
+ + + {editingLabel ? 'Edit Label' : 'Create New Label'} + +
+ { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + form.handleSubmit(handleSubmit)(); + } + }} + > +
+
+ ( + + Label Name + + + + + + )} + /> +
+
+ +
+
+ {LABEL_COLORS.map((color, index) => ( +
+
+
+
+
+ + +
+
+ +
+
+
+ ); +} diff --git a/apps/mail/components/magicui/file-tree.tsx b/apps/mail/components/magicui/file-tree.tsx index 0cb2049f7..5a9f78c7a 100644 --- a/apps/mail/components/magicui/file-tree.tsx +++ b/apps/mail/components/magicui/file-tree.tsx @@ -197,6 +197,7 @@ type FolderProps = { isSelect?: boolean; onFolderClick?: (id: string) => void; hasChildren?: boolean; + color?: number; } & FolderComponentProps; const Folder = forwardRef>( @@ -211,6 +212,7 @@ const Folder = forwardRef
) : (
- +
)} {element} - {count > 0 && - 0 && ( + + {count} + )} - > - {count} - - }
{element && indicator &&
{emailContent && (
@@ -444,16 +450,6 @@ const Thread = memo(
- {threadLabels && ( -
- {!isFolderSent ? ( - - - - ) : null} - {/* {getThreadData.labels ? : null} */} -
- )} ) : null; diff --git a/apps/mail/components/mail/render-labels.tsx b/apps/mail/components/mail/render-labels.tsx index 6746a68dd..bce850765 100644 --- a/apps/mail/components/mail/render-labels.tsx +++ b/apps/mail/components/mail/render-labels.tsx @@ -38,11 +38,15 @@ export const RenderLabels = ({ count = 1, labels }: { count?: number; labels: La key={label.id} onClick={handleFilterByLabel(label)} className={cn( - 'dark:bg-subtleBlack bg-subtleWhite text-primary inline-block overflow-hidden truncate rounded border px-1.5 py-0.5 text-xs font-medium', + 'dark:bg-subtleBlack bg-subtleWhite text-primary inline-block overflow-hidden truncate rounded bg-opacity-10 px-1.5 py-0.5 text-xs font-medium', searchValue.value.includes(`label:${label.name}`) && 'border-neutral-800 dark:border-white', )} - style={{ backgroundColor: label.color?.backgroundColor, color: label.color?.textColor }} + style={{ + background: label.color?.backgroundColor + '1a', + color: label.color?.backgroundColor, + // borderColor: label.color?.backgroundColor, + }} > {label.name} @@ -50,7 +54,7 @@ export const RenderLabels = ({ count = 1, labels }: { count?: number; labels: La {hiddenLabels.length > 0 && ( - @@ -60,13 +64,14 @@ export const RenderLabels = ({ count = 1, labels }: { count?: number; labels: La key={label.id} onClick={handleFilterByLabel(label)} className={cn( - 'dark:bg-subtleBlack bg-subtleWhite inline-block truncate rounded border px-1.5 py-0.5 text-xs font-medium overflow-hidden', + 'dark:bg-subtleBlack bg-subtleWhite inline-block overflow-hidden truncate rounded border px-1.5 py-0.5 text-xs font-medium', searchValue.value.includes(`label:${label.name}`) && 'border-neutral-800 dark:border-white', )} style={{ backgroundColor: label.color?.backgroundColor, color: label.color?.textColor, + borderColor: label.color?.backgroundColor, }} > {label.name} diff --git a/apps/mail/components/ui/nav-main.tsx b/apps/mail/components/ui/nav-main.tsx index f6a6e888e..8a632ae3e 100644 --- a/apps/mail/components/ui/nav-main.tsx +++ b/apps/mail/components/ui/nav-main.tsx @@ -13,17 +13,13 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { - SidebarGroup, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from './sidebar'; +import { SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from './sidebar'; import { Collapsible, CollapsibleTrigger } from '@/components/ui/collapsible'; import { useActiveConnection, useConnections } from '@/hooks/use-connections'; import { type MessageKey, type NavItem } from '@/config/navigation'; +import { LabelDialog } from '@/components/labels/label-dialog'; import { useSearchValue } from '@/hooks/use-search-value'; +import { useSidebar } from '../context/sidebar-context'; import { useTRPC } from '@/providers/query-provider'; import { RecursiveFolder } from './recursive-folder'; import { useMutation } from '@tanstack/react-query'; @@ -35,6 +31,7 @@ import { useSession } from '@/lib/auth-client'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { useStats } from '@/hooks/use-stats'; +import SidebarLabels from './sidebar-labels'; import { CurvedArrow } from '../icons/icons'; import { Command, Plus } from 'lucide-react'; import { Tree } from '../magicui/file-tree'; @@ -42,6 +39,7 @@ import { useCallback, useRef } from 'react'; import { BASE_URL } from '@/lib/constants'; import { useTranslations } from 'use-intl'; import { useForm } from 'react-hook-form'; +import type { Label } from '@/types'; import { useQueryState } from 'nuqs'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; @@ -78,23 +76,16 @@ export function NavMain({ items }: NavMainProps) { const pathname = location.pathname; const searchParams = new URLSearchParams(); const [category] = useQueryState('category'); - const [searchValue, setSearchValue] = useSearchValue(); + const [isDialogOpen, setIsDialogOpen] = React.useState(false); const { data: session } = useSession(); const { data: connections } = useConnections(); const { data: stats } = useStats(); const { data: activeConnection } = useActiveConnection(); - const form = useForm({ - defaultValues: { - name: '', - color: { backgroundColor: '', textColor: '#ffffff' }, - }, - }); const trpc = useTRPC(); const { mutateAsync: createLabel } = useMutation(trpc.labels.create.mutationOptions()); - const formColor = form.watch('color'); const { data, refetch } = useLabels(); @@ -172,11 +163,6 @@ export function NavMain({ items }: NavMainProps) { [pathname, category, searchParams, isValidInternalUrl], ); - const getLabelCount = useCallback((labelName: string | undefined): number => { - if (!stats || !labelName) return 0; - return stats.find((stat) => stat.label?.toLowerCase() === labelName.toLowerCase())?.count ?? 0; - }, [stats]); - const activeAccount = React.useMemo(() => { if (!activeConnection?.id || !connections?.connections) return null; return connections.connections.find((connection) => connection.id === activeConnection?.id); @@ -204,54 +190,11 @@ export function NavMain({ items }: NavMainProps) { [pathname, searchParams], ); - const handleFilterByLabel = (label: LabelType) => () => { - const existingValue = searchValue.value; - if (existingValue.includes(`label:${label.name}`)) { - setSearchValue({ - value: existingValue.replace(`label:${label.name}`, ''), - highlight: '', - folder: '', - }); - return; - } - const newValue = existingValue ? `${existingValue} label:${label.name}` : `label:${label.name}`; - setSearchValue({ - value: newValue, - highlight: '', - folder: '', - }); - }; - const onSubmit = async (data: LabelType) => { - if (!data.color?.backgroundColor) { - form.setError('color', { - type: 'required', - message: 'Please select a color', - }); - return; - } - - try { - toast.promise(createLabel(data), { - loading: 'Creating label...', - success: 'Label created successfully', - error: 'Failed to create label', - finally: () => { - refetch(); - }, - }); - } catch (error) { - console.error('Error creating label:', error); - } finally { - handleClose(); - } - }; - - const handleClose = () => { - setIsDialogOpen(false); - form.reset({ - name: '', - color: { backgroundColor: '', textColor: '#ffffff' }, + await toast.promise(createLabel(data), { + loading: 'Creating label...', + success: 'Label created successfully', + error: 'Failed to create label', }); }; @@ -297,8 +240,8 @@ export function NavMain({ items }: NavMainProps) { {activeAccount?.providerId === 'google' ? 'Labels' : 'Folders'} {activeAccount?.providerId === 'google' ? ( - - + - - - - Create New Label - -
- { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - form.handleSubmit(onSubmit)(); - } - }} - > -
-
- ( - - Label Name - - - - - - )} - /> -
-
- ( - - Color - -
-
- {[ - // Row 1 - Grayscale - '#000000', - '#434343', - '#666666', - '#999999', - '#cccccc', - '#ffffff', - // Row 2 - Warm colors - '#fb4c2f', - '#ffad47', - '#fad165', - '#ff7537', - '#cc3a21', - '#8a1c0a', - // Row 3 - Cool colors - '#16a766', - '#43d692', - '#4a86e8', - '#285bac', - '#3c78d8', - '#0d3472', - // Row 4 - Purple tones - '#a479e2', - '#b99aff', - '#653e9b', - '#3d188e', - '#f691b3', - '#994a64', - // Row 5 - Pastels - '#f6c5be', - '#ffe6c7', - '#c6f3de', - '#c9daf8', - ].map((color) => ( -
-
-
- -
- )} - /> -
-
-
- - -
-
- -
-
+ } + onSubmit={onSubmit} + onSuccess={refetch} + /> ) : activeAccount?.providerId === 'microsoft' ? null : null} -
-
- - {(() => { - if (!data) return null; - const isMicrosoftAccount = activeAccount?.providerId === 'microsoft'; - if (isMicrosoftAccount) { - return data?.map((label) => ( - - )); - } - - const groups = { - brackets: [] as typeof data, - other: [] as typeof data, - folders: {} as Record, - }; - - data.forEach((label) => { - if (/\[.*\]/.test(label.name)) { - groups.brackets.push(label); - } else if (/[^/]+\/[^/]+/.test(label.name)) { - const [groupName] = label.name.split('/') as [string]; - if (!groups.folders[groupName]) { - groups.folders[groupName] = []; - } - groups.folders[groupName].push(label); - } else { - groups.other.push(label); - } - }); - - const components = []; - - Object.entries(groups.folders) - .sort(([a], [b]) => a.localeCompare(b)) - .forEach(([groupName, labels]) => { - const groupFolder = { - id: `group-${groupName}`, - name: groupName, - labels: labels.map((label) => ({ - id: label.id, - name: label.name.split('/').slice(1).join('/'), - originalLabel: label, - })), - }; - components.push( - , - ); - }); - - if (groups.other.length > 0) { - groups.other.forEach((label) => { - components.push( - , - ); - }); - } - - if (groups.brackets.length > 0) { - const bracketsFolder = { - id: 'group-other', - name: 'Other', - labels: groups.brackets.map((label) => ({ - id: label.id, - name: label.name.replace(/\[|\]/g, ''), - originalLabel: label, - })), - }; - components.push( - stat.label?.toLowerCase() === bracketsFolder.name?.toLowerCase())?.count : 0} - />, - ); - } - - return components; - })()} - -
-
+ )} diff --git a/apps/mail/components/ui/recursive-folder.tsx b/apps/mail/components/ui/recursive-folder.tsx index c6a5b3a4b..c6e713aa8 100644 --- a/apps/mail/components/ui/recursive-folder.tsx +++ b/apps/mail/components/ui/recursive-folder.tsx @@ -1,20 +1,26 @@ import { useActiveConnection, useConnections } from '@/hooks/use-connections'; import { LabelSidebarContextMenu } from '../context/label-sidebar-context'; import { useSearchValue } from '@/hooks/use-search-value'; +import type { Label, Label as LabelType } from '@/types'; import { useSidebar } from '../context/sidebar-context'; -import type { Label as LabelType } from '@/types'; import { Folder } from '../magicui/file-tree'; import { useNavigate } from 'react-router'; import { useCallback } from 'react'; import * as React from 'react'; -export const RecursiveFolder = ({ label, activeAccount, count }: { label: any; activeAccount?: any; count?: number }) => { +export const RecursiveFolder = ({ + label, + activeAccount, + count, +}: { + label: Label & { originalLabel?: Label }; + activeAccount?: any; + count?: number; +}) => { const [searchValue, setSearchValue] = useSearchValue(); const isActive = searchValue.value.includes(`label:${label.name}`); const isFolderActive = isActive || window.location.pathname.includes(`/mail/label/${label.id}`); const navigate = useNavigate(); - const { data: connections } = useConnections(); - const { data: activeConnection } = useActiveConnection(); const { setOpenMobile, isMobile } = useSidebar(); const handleFilterByLabel = useCallback( @@ -48,7 +54,7 @@ export const RecursiveFolder = ({ label, activeAccount, count }: { label: any; a return; } - const labelToUse = label.originalLabel || label; + const labelToUse = label; if (activeAccount.providerId === 'microsoft') { navigate(`/mail/${id}`); @@ -78,10 +84,16 @@ export const RecursiveFolder = ({ label, activeAccount, count }: { label: any; a hasChildren={hasChildren} onFolderClick={handleFolderClick} isSelect={isFolderActive} - count={count} + count={count || 0} + className="max-w-[192px]" > {label.labels?.map((childLabel: any) => ( - + ))} diff --git a/apps/mail/components/ui/sidebar-labels.tsx b/apps/mail/components/ui/sidebar-labels.tsx new file mode 100644 index 000000000..5c04be15b --- /dev/null +++ b/apps/mail/components/ui/sidebar-labels.tsx @@ -0,0 +1,157 @@ +import { RecursiveFolder } from './recursive-folder'; +import type { Label as LabelType } from '@/types'; +import { Tree } from '../magicui/file-tree'; +import { useCallback } from 'react'; + +type Props = { + data: LabelType[]; + activeAccount: + | { + id: string; + email: string; + name: string | null; + picture: string | null; + createdAt: Date; + providerId: 'google' | 'microsoft'; + } + | null + | undefined; + stats: + | { + count?: number; + label?: string; + }[] + | undefined; +}; + +const SidebarLabels = ({ data, activeAccount, stats }: Props) => { + const getLabelCount = useCallback( + (labelName: string | undefined): number => { + if (!stats || !labelName) return 0; + return ( + stats.find((stat) => stat.label?.toLowerCase() === labelName.toLowerCase())?.count ?? 0 + ); + }, + [stats], + ); + + return ( +
+
+ + {(() => { + if (!data) return null; + const isMicrosoftAccount = activeAccount?.providerId === 'microsoft'; + if (isMicrosoftAccount) { + return data?.map((label) => ( + + )); + } + + const groups = { + brackets: [] as typeof data, + other: [] as typeof data, + folders: {} as Record, + }; + + data.forEach((label) => { + if (/\[.*\]/.test(label.name)) { + groups.brackets.push(label); + } else if (/[^/]+\/[^/]+/.test(label.name)) { + const [groupName] = label.name.split('/') as [string]; + if (!groups.folders[groupName]) { + groups.folders[groupName] = []; + } + groups.folders[groupName].push(label); + } else { + groups.other.push(label); + } + }); + + const components = []; + + Object.entries(groups.folders) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([groupName, labels]) => { + const groupFolder = { + id: `group-${groupName}`, + name: groupName, + type: 'folder', + labels: labels.map((label) => ({ + id: label.id, + name: label.name.split('/').slice(1).join('/'), + type: label.type, + originalLabel: label, + })), + }; + components.push( + , + ); + }); + + if (groups.other.length > 0) { + groups.other.forEach((label) => { + components.push( + , + ); + }); + } + + if (groups.brackets.length > 0) { + const bracketsFolder = { + id: 'group-other', + name: 'Other', + type: 'folder', + labels: groups.brackets.map((label) => ({ + id: label.id, + name: label.name.replace(/\[|\]/g, ''), + type: label.type, + originalLabel: label, + })), + }; + components.push( + + stat.label?.toLowerCase() === bracketsFolder.name?.toLowerCase(), + )?.count + : 0 + } + />, + ); + } + + return components; + })()} + +
+
+ ); +}; + +export default SidebarLabels; diff --git a/apps/mail/components/ui/sidebar.tsx b/apps/mail/components/ui/sidebar.tsx index 8c26a35e7..9fed53028 100644 --- a/apps/mail/components/ui/sidebar.tsx +++ b/apps/mail/components/ui/sidebar.tsx @@ -260,7 +260,7 @@ const SidebarContent = React.forwardRef + labelColor.backgroundColor === color.backgroundColor && + labelColor.textColor === color.textColor, + ); +} + +export const LABEL_BACKGROUND_COLORS = LABEL_COLORS.map((color) => color.backgroundColor); diff --git a/apps/mail/types/index.ts b/apps/mail/types/index.ts index ec0585915..fc1b05a31 100644 --- a/apps/mail/types/index.ts +++ b/apps/mail/types/index.ts @@ -6,6 +6,7 @@ export type Label = { textColor: string; }; type: string; + labels?: Label[]; }; export interface User { diff --git a/apps/server/src/lib/driver/google-label-color-map.ts b/apps/server/src/lib/driver/google-label-color-map.ts new file mode 100644 index 000000000..da47171e7 --- /dev/null +++ b/apps/server/src/lib/driver/google-label-color-map.ts @@ -0,0 +1,82 @@ +type ColorMapping = { + backgroundColor: string; + textColor: string; +}; + +export const GOOGLE_LABEL_COLOR_MAP: Record = { + // TODO: Add your custom color mappings here + // Example format: + // '#ffffff|#000000': { backgroundColor: '#yourCustomBg', textColor: '#yourCustomText' }, + + // Grayscale + '#000000|#ffffff': { backgroundColor: '#000000', textColor: '#ffffff' }, + '#434343|#ffffff': { backgroundColor: '#434343', textColor: '#ffffff' }, + '#666666|#ffffff': { backgroundColor: '#666666', textColor: '#ffffff' }, + '#999999|#ffffff': { backgroundColor: '#999999', textColor: '#ffffff' }, + '#cccccc|#000000': { backgroundColor: '#cccccc', textColor: '#000000' }, + '#ffffff|#000000': { backgroundColor: '#ffffff', textColor: '#000000' }, + + // Warm colors + '#fb4c2f|#ffffff': { backgroundColor: '#fb4c2f', textColor: '#ffffff' }, + '#ffad47|#ffffff': { backgroundColor: '#ffad47', textColor: '#ffffff' }, + '#fad165|#000000': { backgroundColor: '#fad165', textColor: '#000000' }, + '#ff7537|#ffffff': { backgroundColor: '#ff7537', textColor: '#ffffff' }, + '#cc3a21|#ffffff': { backgroundColor: '#cc3a21', textColor: '#ffffff' }, + '#8a1c0a|#ffffff': { backgroundColor: '#8a1c0a', textColor: '#ffffff' }, + + // Cool colors + '#16a766|#ffffff': { backgroundColor: '#16a766', textColor: '#ffffff' }, + '#43d692|#ffffff': { backgroundColor: '#43d692', textColor: '#ffffff' }, + '#4a86e8|#ffffff': { backgroundColor: '#4a86e8', textColor: '#ffffff' }, + '#285bac|#ffffff': { backgroundColor: '#285bac', textColor: '#ffffff' }, + '#3c78d8|#ffffff': { backgroundColor: '#3c78d8', textColor: '#ffffff' }, + '#0d3472|#ffffff': { backgroundColor: '#0d3472', textColor: '#ffffff' }, + + // Purple tones + '#a479e2|#ffffff': { backgroundColor: '#a479e2', textColor: '#ffffff' }, + '#b99aff|#ffffff': { backgroundColor: '#b99aff', textColor: '#ffffff' }, + '#653e9b|#ffffff': { backgroundColor: '#653e9b', textColor: '#ffffff' }, + '#3d188e|#ffffff': { backgroundColor: '#3d188e', textColor: '#ffffff' }, + '#f691b3|#ffffff': { backgroundColor: '#f691b3', textColor: '#ffffff' }, + '#994a64|#ffffff': { backgroundColor: '#994a64', textColor: '#ffffff' }, + + // Pastels + '#f6c5be|#000000': { backgroundColor: '#f6c5be', textColor: '#000000' }, + '#ffe6c7|#000000': { backgroundColor: '#ffe6c7', textColor: '#000000' }, + '#c6f3de|#000000': { backgroundColor: '#c6f3de', textColor: '#000000' }, + '#c9daf8|#000000': { backgroundColor: '#c9daf8', textColor: '#000000' }, +}; +export function mapGoogleLabelColor( + googleColor: ColorMapping | undefined, +): ColorMapping | undefined { + if (!googleColor || !googleColor.backgroundColor || !googleColor.textColor) { + return googleColor; + } + + const key = `${googleColor.backgroundColor}|${googleColor.textColor}`; + const mappedColor = GOOGLE_LABEL_COLOR_MAP[key]; + + return mappedColor || googleColor; +} + +export function mapToGoogleLabelColor( + customColor: ColorMapping | undefined, +): ColorMapping | undefined { + if (!customColor || !customColor.backgroundColor || !customColor.textColor) { + return customColor; + } + + for (const [googleKey, mappedValue] of Object.entries(GOOGLE_LABEL_COLOR_MAP)) { + if ( + mappedValue.backgroundColor === customColor.backgroundColor && + mappedValue.textColor === customColor.textColor + ) { + const parts = googleKey.split('|'); + const backgroundColor = parts[0] || ''; + const textColor = parts[1] || ''; + return { backgroundColor, textColor }; + } + } + + return customColor; +} diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index bd13d4635..aaeca5862 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -8,6 +8,7 @@ import { sanitizeContext, StandardizedError, } from './utils'; +import { mapGoogleLabelColor, mapToGoogleLabelColor } from './google-label-color-map'; import { parseAddressList, parseFrom, wasSentWithTLS } from '../email-utils'; import type { IOutgoingMessage, Label, ParsedMessage } from '../../types'; import { sanitizeTipTapHtml } from '../sanitize-tip-tap-html'; @@ -16,8 +17,8 @@ import { type gmail_v1, gmail } from '@googleapis/gmail'; import { OAuth2Client } from 'google-auth-library'; import type { CreateDraftData } from '../schemas'; import { createMimeMessage } from 'mimetext'; -import { cleanSearchValue } from '../utils'; import { people } from '@googleapis/people'; +import { cleanSearchValue } from '../utils'; import { env } from 'cloudflare:workers'; import * as he from 'he'; @@ -574,10 +575,10 @@ export class GoogleMailManager implements MailManager { id: label.id ?? '', name: label.name ?? '', type: label.type ?? '', - color: { + color: mapGoogleLabelColor({ backgroundColor: label.color?.backgroundColor ?? '', textColor: label.color?.textColor ?? '', - }, + }), })) ?? [] ); } @@ -589,10 +590,10 @@ export class GoogleMailManager implements MailManager { return { id: labelId, name: res.data.name ?? '', - color: { + color: mapGoogleLabelColor({ backgroundColor: res.data.color?.backgroundColor ?? '', textColor: res.data.color?.textColor ?? '', - }, + }), type: res.data.type ?? 'user', }; } @@ -607,10 +608,10 @@ export class GoogleMailManager implements MailManager { labelListVisibility: 'labelShow', messageListVisibility: 'show', color: label.color - ? { + ? mapToGoogleLabelColor({ backgroundColor: label.color.backgroundColor, textColor: label.color.textColor, - } + }) : undefined, }, }); @@ -622,10 +623,10 @@ export class GoogleMailManager implements MailManager { requestBody: { name: label.name, color: label.color - ? { + ? mapToGoogleLabelColor({ backgroundColor: label.color.backgroundColor, textColor: label.color.textColor, - } + }) : undefined, }, });