import { useCallback } from 'react' ; import { c , msgid } from 'ttag' ; import { useApi , useNotifications , useEventManager , useLabels , useModals , ConfirmModal } from '@proton/components' ; import { labelMessages , unlabelMessages } from '@proton/shared/lib/api/messages' ; import { labelConversations , unlabelConversations } from '@proton/shared/lib/api/conversations' ; import { undoActions } from '@proton/shared/lib/api/mailUndoActions' ; import { MAILBOX_LABEL_IDS } from '@proton/shared/lib/constants' ; import { Message } from '@proton/shared/lib/interfaces/mail/Message' ; import isTruthy from '@proton/shared/lib/helpers/isTruthy' ; import UndoActionNotification from '../components/notifications/UndoActionNotification' ; import { isMessage as testIsMessage } from '../helpers/elements' ; import { getMessagesAuthorizedToMove } from '../helpers/message/messages' ; import { Element } from '../models/element' ; import { useOptimisticApplyLabels } from './optimistic/useOptimisticApplyLabels' ; import { SUCCESS_NOTIFICATION_EXPIRATION } from '../constants' ; import { Conversation } from '../models/conversation' ; const { SPAM , TRASH , SCHEDULED , SENT , ALL_SENT , DRAFTS , ALL_DRAFTS , INBOX } = MAILBOX_LABEL_IDS ; const getNotificationTextStarred = ( isMessage , elementsCount ) => { if ( isMessage ) { if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Message marked as Starred.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } message marked as Starred.` , `${ elementsCount } messages marked as Starred.` , elementsCount ) ; } if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Conversation marked as Starred.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } conversation marked as Starred.` , `${ elementsCount } conversations marked as Starred.` , elementsCount ) ; } ; const getNotificationTextRemoved = ( isMessage , elementsCount , labelName ) => { if ( isMessage ) { if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Message removed from ${ labelName }.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } message removed from ${ labelName }.` , `${ elementsCount } messages removed from ${ labelName }.` , elementsCount ) ; } if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Conversation removed from ${ labelName }.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } conversation removed from ${ labelName }.` , `${ elementsCount } conversations removed from ${ labelName }.` , elementsCount ) ; } ; const getNotificationTextAdded = ( isMessage , elementsCount , labelName ) => { if ( isMessage ) { if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Message added to ${ labelName }.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } message added to ${ labelName }.` , `${ elementsCount } messages added to ${ labelName }.` , elementsCount ) ; } if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Conversation added to ${ labelName }.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } conversation added to ${ labelName }.` , `${ elementsCount } conversations added to ${ labelName }.` , elementsCount ) ; } ; const joinSentences = ( success , notAuthorized ) => [ success , notAuthorized ] . filter ( isTruthy ) . join ( ' ' ) ; const getNotificationTextMoved = ( isMessage , elementsCount , messagesNotAuthorizedToMove , folderName , folderID ? , fromLabelID ? ) => { const notAuthorized = messagesNotAuthorizedToMove ? c ( 'Info' ) . ngettext ( msgid `${ messagesNotAuthorizedToMove } message could not be moved.` , `${ messagesNotAuthorizedToMove } messages could not be moved.` , messagesNotAuthorizedToMove ) : '' ; if ( folderID === SPAM ) { if ( isMessage ) { if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Message moved to spam and sender added to Block List.` ; } return joinSentences ( c ( 'Success' ) . ngettext ( msgid `${ elementsCount } message moved to spam and sender added to Block List.` , `${ elementsCount } messages moved to spam and senders added to Block List.` , elementsCount ) , notAuthorized ) ; } if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Conversation moved to spam and sender added to Block List.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } conversation moved to spam and sender added to Block List.` , `${ elementsCount } conversations moved to spam and senders added to Block List.` , elementsCount ) ; } if ( fromLabelID === SPAM && folderID !== TRASH ) { if ( isMessage ) { if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Message moved to ${ folderName } and sender added to Allow List.` ; } return joinSentences ( c ( 'Success' ) . ngettext ( msgid `${ elementsCount } message moved to ${ folderName } and sender added to Allow List.` , `${ elementsCount } messages moved to ${ folderName } and senders added to Allow List.` , elementsCount ) , notAuthorized ) ; } if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Conversation moved to ${ folderName } and sender added to Allow List.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } conversation moved to ${ folderName } and sender added to Allow List.` , `${ elementsCount } conversations moved to ${ folderName } and senders added to Allow List.` , elementsCount ) ; } if ( isMessage ) { if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Message moved to ${ folderName }.` ; } return joinSentences ( c ( 'Success' ) . ngettext ( msgid `${ elementsCount } message moved to ${ folderName }.` , `${ elementsCount } messages moved to ${ folderName }.` , elementsCount ) , notAuthorized ) ; } if ( elementsCount === 1 ) { return c ( 'Success' ) . t `Conversation moved to ${ folderName }.` ; } return c ( 'Success' ) . ngettext ( msgid `${ elementsCount } conversation moved to ${ folderName }.` , `${ elementsCount } conversations moved to ${ folderName }.` , elementsCount ) ; } ; export const useApplyLabels = ( ) => { const api = useApi ( ) ; const { call , stop , start } = useEventManager ( ) ; const { createNotification } = useNotifications ( ) ; const [ labels = [ ] ] = useLabels ( ) ; const optimisticApplyLabels = useOptimisticApplyLabels ( ) ; const applyLabels = useCallback ( async ( elements , changes , silent = false ) => { if ( ! elements . length ) { return ; } let undoing = false ; const isMessage = testIsMessage ( elements [ 0 ] ) ; const labelAction = isMessage ? labelMessages : labelConversations ; const unlabelAction = isMessage ? unlabelMessages : unlabelConversations ; const changesKeys = Object . keys ( changes ) ; const elementIDs = elements . map ( ( element ) => element . ID ) ; const rollbacks = { } as { [ labelID ] : ( ) => void ; } ; const handleDo = async ( ) => { let tokens = [ ] ; try { stop ( ) ; tokens = await Promise . all ( changesKeys . map ( async ( LabelID ) => { rollbacks [ LabelID ] = optimisticApplyLabels ( elements , { [ LabelID ] : changes [ LabelID ] } ) ; try { const action = changes [ LabelID ] ? labelAction : unlabelAction ; const { UndoToken } = await api ( action ( { LabelID , IDs : elementIDs } ) ) ; return UndoToken . Token ; } catch ( error ) { rollbacks [ LabelID ] ( ) ; throw error ; } } ) ) ; } finally { if ( ! undoing ) { start ( ) ; await call ( ) ; } } return tokens ; } ; const promise = handleDo ( ) ; const handleUndo = async ( ) => { try { undoing = true ; const tokens = await promise ; stop ( ) ; Object . values ( rollbacks ) . forEach ( ( rollback ) => rollback ( ) ) ; const filteredTokens = tokens . filter ( isTruthy ) ; await Promise . all ( filteredTokens . map ( ( token ) => api ( undoActions ( token ) ) ) ) ; } finally { start ( ) ; await call ( ) ; } } ; let notificationText = c ( 'Success' ) . t `Labels applied.` ; const elementsCount = elementIDs . length ; if ( changesKeys . length === 1 ) { const labelName = labels . filter ( ( l ) => l . ID === changesKeys [ 0 ] ) [ 0 ] ?. Name ; if ( changesKeys [ 0 ] === MAILBOX_LABEL_IDS . STARRED ) { notificationText = getNotificationTextStarred ( isMessage , elementsCount ) ; } else if ( ! Object . values ( changes ) [ 0 ] ) { notificationText = getNotificationTextRemoved ( isMessage , elementsCount , labelName ) ; } else { notificationText = getNotificationTextAdded ( isMessage , elementsCount , labelName ) ; } } if ( ! silent ) { createNotification ( { text : < UndoActionNotification onUndo = { handleUndo } > { notificationText } < /UndoActionNotification>, expiration : SUCCESS_NOTIFICATION_EXPIRATION , } ) ; } } , [ labels ] ) ; return applyLabels ; } ; export const useMoveToFolder = ( ) => { const api = useApi ( ) ; const { call , stop , start } = useEventManager ( ) ; const { createNotification } = useNotifications ( ) ; const [ labels = [ ] ] = useLabels ( ) ; const optimisticApplyLabels = useOptimisticApplyLabels ( ) ; const { createModal } = useModals ( ) ; let canUndo = true ; const searchForScheduled = async ( folderID , isMessage , elements ) => { if ( folderID === TRASH ) { let numberOfScheduledMessages ; if ( isMessage ) { numberOfScheduledMessages = ( elements as Message [ ] ) . filter ( ( element ) => element . LabelIDs . includes ( SCHEDULED ) ) . length ; } else { numberOfScheduledMessages = ( elements as Conversation [ ] ) . filter ( ( element ) => element . Labels ?. some ( ( label ) => label . ID === SCHEDULED ) ) . length ; } if ( numberOfScheduledMessages > 0 && numberOfScheduledMessages === elements . length ) { canUndo = false ; } if ( ! canUndo ) { const text = isMessage ? c ( 'Info' ) . t `Scheduled send of this message will be cancelled.` : c ( 'Info' ) . t `This conversation contains a scheduled message. Sending of this message will be cancelled.` ; await new Promise < void > ( ( resolve , reject ) => { createModal ( < ConfirmModal onConfirm = { resolve } onClose = { reject } title = { c ( 'Title' ) . t `Moving a scheduled message` } confirm = { c ( 'Action' ) . t `OK` } cancel = { c ( 'Action' ) . t `Cancel` } > { text } < /ConfirmModal>); } ) ; } } } ; const moveToFolder = useCallback ( async ( elements , folderID , folderName , fromLabelID , silent = false ) => { if ( ! elements . length ) { return ; } let undoing = false ; const isMessage = testIsMessage ( elements [ 0 ] ) ; await searchForScheduled ( folderID , isMessage , elements ) ; const action = isMessage ? labelMessages : labelConversations ; const authorizedToMove = isMessage ? getMessagesAuthorizedToMove ( elements as Message [ ] , folderID ) : elements ; const elementIDs = authorizedToMove . map ( ( element ) => element . ID ) ; const getNotificationText = ( ) => { let notificationText = c ( 'Error display when performing invalid move on message' ) . t `This action cannot be performed` ; if ( fromLabelID === SENT || fromLabelID === ALL_SENT ) { if ( folderID === INBOX ) { notificationText = c ( 'Error display when performing invalid move on message' ) . t `Sent messages cannot be moved to Inbox` ; } else if ( folderID === SPAM ) { notificationText = c ( 'Error display when performing invalid move on message' ) . t `Sent messages cannot be moved to Spam` ; } } else if ( fromLabelID === DRAFTS || fromLabelID === ALL_DRAFTS ) { if ( folderID === INBOX ) { notificationText = c ( 'Error display when performing invalid move on message' ) . t `Drafts cannot be moved to Inbox` ; } else if ( folderID === SPAM ) { notificationText = c ( 'Error display when performing invalid move on message' ) . t `Drafts cannot be moved to Spam` ; } } return notificationText ; } ; if ( ! authorizedToMove . length ) { createNotification ( { text : getNotificationText ( ) , type : 'error' , } ) ; return ; } const rollback = optimisticApplyLabels ( authorizedToMove , { [ folderID ] : true } , true , [ ] , fromLabelID ) ; const handleDo = async ( ) => { let token ; try { stop ( ) ; const { UndoToken } = await api ( action ( { LabelID : folderID , IDs : elementIDs } ) ) ; token = UndoToken . Token ; } catch ( error ) { rollback ( ) ; } finally { if ( ! undoing ) { start ( ) ; await call ( ) ; } } return token ; } ; const promise = handleDo ( ) ; if ( ! silent ) { const notificationText = getNotificationTextMoved ( isMessage , authorizedToMove . length , elements . length - authorizedToMove . length , folderName , folderID , fromLabelID ) ; const handleUndo = async ( ) => { try { undoing = true ; const token = await promise ; stop ( ) ; rollback ( ) ; await api ( undoActions ( token ) ) ; } finally { start ( ) ; await call ( ) ; } } ; createNotification ( { text : ( < UndoActionNotification onUndo = { canUndo ? handleUndo : undefined } > { notificationText } < /UndoActionNotification>), expiration : SUCCESS_NOTIFICATION_EXPIRATION , } ) ; } } , [ labels ] ) ; return moveToFolder ; } ; export const useStar = ( ) => { const api = useApi ( ) ; const { call , stop , start } = useEventManager ( ) ; const optimisticApplyLabels = useOptimisticApplyLabels ( ) ; const star = useCallback ( async ( elements , value ) => { if ( ! elements . length ) { return ; } const isMessage = testIsMessage ( elements [ 0 ] ) ; const labelAction = isMessage ? labelMessages : labelConversations ; const unlabelAction = isMessage ? unlabelMessages : unlabelConversations ; const action = value ? labelAction : unlabelAction ; const rollback = optimisticApplyLabels ( elements , { [ MAILBOX_LABEL_IDS . STARRED ] : value } ) ; try { stop ( ) ; await api ( action ( { LabelID : MAILBOX_LABEL_IDS . STARRED , IDs : elements . map ( ( element ) => element . ID ) } ) ) ; } catch ( error ) { rollback ( ) ; throw error ; } finally { start ( ) ; await call ( ) ; } } , [ ] ) ; return star ; } ;