@@ -27,8 +27,8 @@ import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-st
2727import { useOptimisticActions } from '@/hooks/use-optimistic-actions' ;
2828import { type ThreadDestination } from '@/lib/thread-actions' ;
2929import { useThread , useThreads } from '@/hooks/use-threads' ;
30- import { ExclamationCircle , Mail } from '../icons/icons' ;
31- import { useMemo , type ReactNode } from 'react' ;
30+ import { ExclamationCircle , Mail , Clock } from '../icons/icons' ;
31+ import { useMemo , type ReactNode , useState } from 'react' ;
3232import { useLabels } from '@/hooks/use-labels' ;
3333import { FOLDERS , LABELS } from '@/lib/utils' ;
3434import { useMail } from '../mail/use-mail' ;
@@ -37,6 +37,7 @@ import { m } from '@/paraglide/messages';
3737import { useParams } from 'react-router' ;
3838import { useQueryState } from 'nuqs' ;
3939import { toast } from 'sonner' ;
40+ import { SnoozeDialog } from '@/components/mail/snooze-dialog' ;
4041
4142interface EmailAction {
4243 id : string ;
@@ -68,12 +69,11 @@ const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected
6869
6970 if ( ! labels || ! thread ) return null ;
7071
71- const handleToggleLabel = async ( labelId : string ) => {
72+ const handleToggleLabel = ( labelId : string ) => {
7273 if ( ! labelId ) return ;
7374
74- let shouldAddLabel = false ;
75-
76- let hasLabel = thread . labels ?. map ( ( label ) => label . id ) . includes ( labelId ) || false ;
75+ // Determine current label state considering optimistic updates
76+ let hasLabel = thread ! . labels ?. some ( ( l ) => l . id === labelId ) ?? false ;
7777
7878 if ( rightClickedThreadOptimisticState . optimisticLabels ) {
7979 if ( rightClickedThreadOptimisticState . optimisticLabels . addedLabelIds . includes ( labelId ) ) {
@@ -85,24 +85,21 @@ const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected
8585 }
8686 }
8787
88- shouldAddLabel = ! hasLabel ;
89-
90- optimisticToggleLabel ( targetThreadIds , labelId , shouldAddLabel ) ;
88+ optimisticToggleLabel ( targetThreadIds , labelId , ! hasLabel ) ;
9189 } ;
9290
9391 return (
9492 < >
9593 { labels
9694 . filter ( ( label ) => label . id )
9795 . map ( ( label ) => {
98- let isChecked = label . id ? thread . labels ?. map ( ( l ) => l . id ) . includes ( label . id ) : false ;
96+ let isChecked = label . id ? thread ! . labels ?. some ( ( l ) => l . id === label . id ) ?? false : false ;
9997
100- const checkboxOptimisticState = useOptimisticThreadState ( threadId ) ;
101- if ( label . id && checkboxOptimisticState . optimisticLabels ) {
102- if ( checkboxOptimisticState . optimisticLabels . addedLabelIds . includes ( label . id ) ) {
98+ if ( rightClickedThreadOptimisticState . optimisticLabels ) {
99+ if ( rightClickedThreadOptimisticState . optimisticLabels . addedLabelIds . includes ( label . id ) ) {
103100 isChecked = true ;
104101 } else if (
105- checkboxOptimisticState . optimisticLabels . removedLabelIds . includes ( label . id )
102+ rightClickedThreadOptimisticState . optimisticLabels . removedLabelIds . includes ( label . id )
106103 ) {
107104 isChecked = false ;
108105 }
@@ -138,7 +135,7 @@ export function ThreadContextMenu({
138135 const [ { isLoading, isFetching } ] = useThreads ( ) ;
139136 const currentFolder = folder ?? '' ;
140137 const isArchiveFolder = currentFolder === FOLDERS . ARCHIVE ;
141-
138+ const isSnoozedFolder = currentFolder === FOLDERS . SNOOZED ;
142139 const [ , setMode ] = useQueryState ( 'mode' ) ;
143140 const [ , setThreadId ] = useQueryState ( 'threadId' ) ;
144141 const { data : threadData } = useThread ( threadId ) ;
@@ -151,6 +148,8 @@ export function ThreadContextMenu({
151148 optimisticMarkAsRead,
152149 optimisticMarkAsUnread,
153150 optimisticDeleteThreads,
151+ optimisticSnooze,
152+ optimisticUnsnooze,
154153 } = useOptimisticActions ( ) ;
155154
156155 const { isUnread, isStarred, isImportant } = useMemo ( ( ) => {
@@ -359,6 +358,31 @@ export function ThreadContextMenu({
359358 ] ;
360359 }
361360
361+ if ( isSnoozedFolder ) {
362+ return [
363+ {
364+ id : 'unsnooze' ,
365+ label : 'Unsnooze' ,
366+ icon : < Inbox className = "mr-2.5 h-4 w-4 opacity-60" /> ,
367+ action : ( ) => {
368+ const targets = mail . bulkSelected . length ? mail . bulkSelected : [ threadId ] ;
369+ optimisticUnsnooze ( targets , currentFolder ) ;
370+ if ( mail . bulkSelected . length ) {
371+ setMail ( { ...mail , bulkSelected : [ ] } ) ;
372+ }
373+ } ,
374+ disabled : false ,
375+ } ,
376+ {
377+ id : 'move-to-bin' ,
378+ label : m [ 'common.mail.moveToBin' ] ( ) ,
379+ icon : < Trash className = "mr-2.5 h-4 w-4 opacity-60" /> ,
380+ action : handleMove ( LABELS . SNOOZED , LABELS . TRASH ) ,
381+ disabled : false ,
382+ } ,
383+ ] ;
384+ }
385+
362386 if ( isArchiveFolder || ! isInbox ) {
363387 return [
364388 {
@@ -422,6 +446,14 @@ export function ThreadContextMenu({
422446 ] ;
423447 } , [ isSpam , isBin , isArchiveFolder , isInbox , isSent , handleMove , handleDelete ] ) ;
424448
449+ const [ snoozeOpen , setSnoozeOpen ] = useState ( false ) ;
450+
451+ const handleSnoozeConfirm = ( wakeAt : Date ) => {
452+ const targets = mail . bulkSelected . length ? mail . bulkSelected : [ threadId ] ;
453+ optimisticSnooze ( targets , currentFolder , wakeAt ) ;
454+ setSnoozeOpen ( false ) ;
455+ } ;
456+
425457 const otherActions : EmailAction [ ] = useMemo (
426458 ( ) => [
427459 {
@@ -453,8 +485,23 @@ export function ThreadContextMenu({
453485 ) ,
454486 action : handleFavorites ,
455487 } ,
488+ {
489+ id : 'snooze' ,
490+ label : 'Snooze' ,
491+ icon : < Clock className = "mr-2.5 h-4 w-4 opacity-60" /> ,
492+ action : ( ) => setSnoozeOpen ( true ) ,
493+ disabled : false ,
494+ } ,
495+ ] ,
496+ [
497+ isUnread ,
498+ isImportant ,
499+ isStarred ,
500+ m ,
501+ handleReadUnread ,
502+ handleToggleImportant ,
503+ handleFavorites ,
456504 ] ,
457- [ isUnread , isImportant , isStarred , m , handleReadUnread , handleToggleImportant , handleFavorites ] ,
458505 ) ;
459506
460507 const renderAction = ( action : EmailAction ) => {
@@ -473,36 +520,43 @@ export function ThreadContextMenu({
473520 } ;
474521
475522 return (
476- < ContextMenu >
477- < ContextMenuTrigger disabled = { isLoading || isFetching } className = "w-full" >
478- { children }
479- </ ContextMenuTrigger >
480- < ContextMenuContent
481- className = "dark:bg-panelDark w-56 overflow-y-auto bg-white"
482- onContextMenu = { ( e ) => e . preventDefault ( ) }
483- >
484- { primaryActions . map ( renderAction ) }
485-
486- < ContextMenuSeparator className = "bg-[#E7E7E7] dark:bg-[#252525]" />
487-
488- < ContextMenuSub >
489- < ContextMenuSubTrigger className = "font-normal" >
490- < Tag className = "mr-2.5 h-4 w-4 opacity-60" />
491- { m [ 'common.mail.labels' ] ( ) }
492- </ ContextMenuSubTrigger >
493- < ContextMenuSubContent className = "dark:bg-panelDark max-h-[520px] w-48 overflow-y-auto bg-white" >
494- < LabelsList threadId = { threadId } bulkSelected = { mail . bulkSelected } />
495- </ ContextMenuSubContent >
496- </ ContextMenuSub >
497-
498- < ContextMenuSeparator className = "bg-[#E7E7E7] dark:bg-[#252525]" />
499-
500- { getActions . map ( renderAction ) }
501-
502- < ContextMenuSeparator className = "bg-[#E7E7E7] dark:bg-[#252525]" />
503-
504- { otherActions . map ( renderAction ) }
505- </ ContextMenuContent >
506- </ ContextMenu >
523+ < >
524+ < ContextMenu >
525+ < ContextMenuTrigger disabled = { isLoading || isFetching } className = "w-full" >
526+ { children }
527+ </ ContextMenuTrigger >
528+ < ContextMenuContent
529+ className = "dark:bg-panelDark w-56 overflow-y-auto bg-white"
530+ onContextMenu = { ( e ) => e . preventDefault ( ) }
531+ >
532+ { primaryActions . map ( renderAction ) }
533+
534+ < ContextMenuSeparator className = "bg-[#E7E7E7] dark:bg-[#252525]" />
535+
536+ < ContextMenuSub >
537+ < ContextMenuSubTrigger className = "font-normal" >
538+ < Tag className = "mr-2.5 h-4 w-4 opacity-60" />
539+ { m [ 'common.mail.labels' ] ( ) }
540+ </ ContextMenuSubTrigger >
541+ < ContextMenuSubContent className = "dark:bg-panelDark max-h-[520px] w-48 overflow-y-auto bg-white" >
542+ < LabelsList threadId = { threadId } bulkSelected = { mail . bulkSelected } />
543+ </ ContextMenuSubContent >
544+ </ ContextMenuSub >
545+
546+ < ContextMenuSeparator className = "bg-[#E7E7E7] dark:bg-[#252525]" />
547+
548+ { getActions . map ( renderAction ) }
549+
550+ < ContextMenuSeparator className = "bg-[#E7E7E7] dark:bg-[#252525]" />
551+
552+ { otherActions . map ( renderAction ) }
553+ </ ContextMenuContent >
554+ </ ContextMenu >
555+ < SnoozeDialog
556+ open = { snoozeOpen }
557+ onOpenChange = { setSnoozeOpen }
558+ onConfirm = { handleSnoozeConfirm }
559+ />
560+ </ >
507561 ) ;
508562}
0 commit comments