@@ -21,15 +21,17 @@ import {
2121 Star ,
2222 StarOff ,
2323 Tag ,
24+ Plus ,
2425 Trash ,
2526} from 'lucide-react' ;
2627import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state' ;
28+ import { LabelDialog } from '@/components/labels/label-dialog' ;
2729import { useOptimisticActions } from '@/hooks/use-optimistic-actions' ;
2830import { ExclamationCircle , Mail , Clock } from '../icons/icons' ;
2931import { SnoozeDialog } from '@/components/mail/snooze-dialog' ;
3032import { type ThreadDestination } from '@/lib/thread-actions' ;
3133import { useThread , useThreads } from '@/hooks/use-threads' ;
32- import { useMemo , type ReactNode , useState } from 'react' ;
34+ import { useMemo , type ReactNode , useState , useCallback } from 'react' ;
3335import { useTRPC } from '@/providers/query-provider' ;
3436import { useMutation } from '@tanstack/react-query' ;
3537import { useLabels } from '@/hooks/use-labels' ;
@@ -40,6 +42,7 @@ import { m } from '@/paraglide/messages';
4042import { useParams } from 'react-router' ;
4143import { useQueryState } from 'nuqs' ;
4244import { toast } from 'sonner' ;
45+ import type { Label as LabelType } from '@/types' ;
4346
4447interface EmailAction {
4548 id : string ;
@@ -61,7 +64,7 @@ interface EmailContextMenuProps {
6164 refreshCallback ?: ( ) => void ;
6265}
6366
64- const LabelsList = ( { threadId, bulkSelected } : { threadId : string ; bulkSelected : string [ ] } ) => {
67+ const LabelsList = ( { threadId, bulkSelected, onCreateLabel } : { threadId : string ; bulkSelected : string [ ] ; onCreateLabel : ( ) => void } ) => {
6568 const { userLabels : labels } = useLabels ( ) ;
6669 const { optimisticToggleLabel } = useOptimisticActions ( ) ;
6770 const targetThreadIds = bulkSelected . length > 0 ? bulkSelected : [ threadId ] ;
@@ -90,6 +93,19 @@ const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected
9093 optimisticToggleLabel ( targetThreadIds , labelId , ! hasLabel ) ;
9194 } ;
9295
96+ // If no labels exist, show create label button
97+ if ( ! labels || labels . length === 0 ) {
98+ return (
99+ < ContextMenuItem
100+ onClick = { onCreateLabel }
101+ className = "font-normal"
102+ >
103+ < Plus className = "mr-2 h-4 w-4 opacity-60" />
104+ { m [ 'common.mail.createNewLabel' ] ( ) }
105+ </ ContextMenuItem >
106+ ) ;
107+ }
108+
93109 return (
94110 < >
95111 { labels
@@ -148,6 +164,7 @@ export function ThreadContextMenu({
148164 const [ , setActiveReplyId ] = useQueryState ( 'activeReplyId' ) ;
149165 const optimisticState = useOptimisticThreadState ( threadId ) ;
150166 const trpc = useTRPC ( ) ;
167+ const { refetch : refetchLabels } = useLabels ( ) ;
151168 const {
152169 optimisticMoveThreadsTo,
153170 optimisticToggleStar,
@@ -159,6 +176,7 @@ export function ThreadContextMenu({
159176 optimisticUnsnooze,
160177 } = useOptimisticActions ( ) ;
161178 const { mutateAsync : deleteThread } = useMutation ( trpc . mail . delete . mutationOptions ( ) ) ;
179+ const { mutateAsync : createLabel } = useMutation ( trpc . labels . create . mutationOptions ( ) ) ;
162180
163181 const { isUnread, isStarred, isImportant } = useMemo ( ( ) => {
164182 const unread = threadData ?. hasUnread ?? false ;
@@ -454,13 +472,47 @@ export function ThreadContextMenu({
454472 } , [ isSpam , isBin , isArchiveFolder , isInbox , isSent , handleMove , handleDelete ] ) ;
455473
456474 const [ snoozeOpen , setSnoozeOpen ] = useState ( false ) ;
475+ const [ createLabelOpen , setCreateLabelOpen ] = useState ( false ) ;
476+
477+ const handleOpenCreateLabel = useCallback ( ( ) => {
478+ setCreateLabelOpen ( true ) ;
479+ } , [ ] ) ;
457480
458481 const handleSnoozeConfirm = ( wakeAt : Date ) => {
459482 const targets = mail . bulkSelected . length ? mail . bulkSelected : [ threadId ] ;
460483 optimisticSnooze ( targets , currentFolder , wakeAt ) ;
461484 setSnoozeOpen ( false ) ;
462485 } ;
463486
487+ const handleCreateLabel = async ( data : LabelType ) => {
488+ const labelData = {
489+ name : data . name ,
490+ color : {
491+ backgroundColor : data . color ?. backgroundColor || '#202020' ,
492+ textColor : data . color ?. textColor || '#FFFFFF'
493+ }
494+ } ;
495+
496+ try {
497+ const promise = createLabel ( labelData ) . then ( async ( result ) => {
498+ await refetchLabels ( ) ;
499+ return result ;
500+ } ) ;
501+
502+ toast . promise ( promise , {
503+ loading : m [ 'common.labels.savingLabel' ] ( ) ,
504+ success : m [ 'common.labels.saveLabelSuccess' ] ( ) ,
505+ error : m [ 'common.labels.failedToSavingLabel' ] ( ) ,
506+ } ) ;
507+
508+ await promise ;
509+ } catch ( error ) {
510+ console . error ( 'Failed to create label:' , error ) ;
511+ } finally {
512+ setCreateLabelOpen ( false ) ;
513+ }
514+ } ;
515+
464516 const otherActions : EmailAction [ ] = useMemo (
465517 ( ) => [
466518 {
@@ -520,6 +572,11 @@ export function ThreadContextMenu({
520572
521573 return (
522574 < >
575+ < LabelDialog
576+ open = { createLabelOpen }
577+ onOpenChange = { setCreateLabelOpen }
578+ onSubmit = { handleCreateLabel }
579+ />
523580 < ContextMenu >
524581 < ContextMenuTrigger disabled = { isLoading || isFetching } className = "w-full" >
525582 { children }
@@ -538,7 +595,7 @@ export function ThreadContextMenu({
538595 { m [ 'common.mail.labels' ] ( ) }
539596 </ ContextMenuSubTrigger >
540597 < ContextMenuSubContent className = "dark:bg-panelDark max-h-[520px] w-48 overflow-y-auto bg-white" >
541- < LabelsList threadId = { threadId } bulkSelected = { mail . bulkSelected } />
598+ < LabelsList threadId = { threadId } bulkSelected = { mail . bulkSelected } onCreateLabel = { handleOpenCreateLabel } />
542599 </ ContextMenuSubContent >
543600 </ ContextMenuSub >
544601
0 commit comments