import { forwardRef , KeyboardEvent , RefObject , useCallback , useMemo , useRef , useState } from 'react' ; import { canonizeEmail } from '@proton/shared/lib/helpers/email' ; import { ContactEmail , ContactGroup } from '@proton/shared/lib/interfaces/contacts' ; import { Recipient } from '@proton/shared/lib/interfaces' ; import { inputToRecipient } from '@proton/shared/lib/mail/recipient' ; import { SimpleMap } from '@proton/shared/lib/interfaces/utils' ; import { noop } from '@proton/shared/lib/helpers/function' ; import Input , { Props as InputProps } from '../input/Input' ; import { Option } from '../option' ; import { Marks } from '../text' ; import { useAutocomplete , useAutocompleteFilter , AutocompleteList } from '../autocomplete' ; import { getRecipientFromAutocompleteItem , AddressesAutocompleteItem , getContactsAutocompleteItems , getContactGroupsAutocompleteItems , getNumberOfMembersText , GroupsWithContactsMap , } from './helper' ; import Icon from '../icon/Icon' ; import { useCombinedRefs } from '../../hooks' ; interface Props extends Omit < InputProps , 'value' > { id : string ; onKeyDown ? : ( e : KeyboardEvent < HTMLInputElement > ) => void ; onAddRecipients : ( recipients : Recipient [ ] ) => void ; recipients : Recipient [ ] ; anchorRef : RefObject < HTMLElement > ; contactEmails ? : ContactEmail [ ] ; contactGroups ? : ContactGroup [ ] ; contactEmailsMap ? : SimpleMap < ContactEmail > ; groupsWithContactsMap ? : GroupsWithContactsMap ; hasEmailPasting ? : boolean ; hasAddOnBlur ? : boolean ; limit ? : number ; onAddInvalidEmail ? : ( ) => void ; validate ? : ( email : string ) => string | void ; } const AddressesAutocomplete = forwardRef < HTMLInputElement , Props > ( ( { contactEmails , contactEmailsMap , contactGroups , groupsWithContactsMap , id , onKeyDown , recipients , onAddRecipients , anchorRef , hasEmailPasting = false , hasAddOnBlur = false , limit = 20 , onAddInvalidEmail , onChange , validate = noop , ... rest } , ref ) => { const [ input , setInput ] = useState ( '' ) ; const [ emailError , setEmailError ] = useState ( '' ) ; const inputRef = useRef < HTMLInputElement > ( null ) ; const [ recipientsByAddress , recipientsByGroup ] = useMemo ( ( ) => { return recipients . reduce < [ Set < string > , Set < string > ] > ( ( acc , { Address , Group } ) => { if ( Address ) { acc [ 0 ] . add ( canonizeEmail ( Address ) ) ; } if ( Group ) { acc [ 1 ] . add ( Group ) ; } return acc ; } , [ new Set ( ) , new Set ( ) ] ) ; } , [ recipients ] ) ; const isGroupEmpty = ( groupID ) => { return groupsWithContactsMap ? ( groupsWithContactsMap [ groupID ] ?. contacts . length || 0 ) <= 0 : false ; } ; const contactsAutocompleteItems = useMemo ( ( ) => { return [ ... getContactsAutocompleteItems ( contactEmails , ( { Email } ) => ! recipientsByAddress . has ( canonizeEmail ( Email ) ) && ! validate ( Email ) ) , ... getContactGroupsAutocompleteItems ( contactGroups , ( { Path , ID } ) => ! recipientsByGroup . has ( Path ) && ! isGroupEmpty ( ID ) ) , ] ; } , [ contactEmails , contactGroups , recipientsByAddress , recipientsByGroup ] ) ; const options = [ ... contactsAutocompleteItems ] ; const safeAddRecipients = ( newRecipients ) => { const uniqueNewRecipients = newRecipients . filter ( ( { Address } ) => { return ! validate ( Address || '' ) ; } ) ; if ( ! uniqueNewRecipients . length ) { return ; } onAddRecipients ( uniqueNewRecipients ) ; } ; const handleAddRecipient = ( newRecipients ) => { setInput ( '' ) ; setEmailError ( '' ) ; safeAddRecipients ( newRecipients ) ; } ; const handleAddRecipientFromInput = ( input ) => { const trimmedInput = input . trim ( ) ; if ( ! trimmedInput . length ) { setInput ( '' ) ; return ; } const newRecipient = inputToRecipient ( trimmedInput ) ; const error = validate ( newRecipient . Address || '' ) ; if ( ! error ) { handleAddRecipient ( [ newRecipient ] ) ; } else { onAddInvalidEmail ?. ( ) ; setEmailError ( error ) ; } } ; const handleSelect = ( item ) => { handleAddRecipient ( getRecipientFromAutocompleteItem ( contactEmails , item ) ) ; } ; const getData = useCallback ( ( value ) => { return value . label ; } , [ ] ) ; const filteredOptions = useAutocompleteFilter ( input , options , getData , limit , 1 ) ; const exactNameGroup = filteredOptions . find ( ( { option : { label , type } } ) => label === input && type === 'group' ) ; const filteredAndSortedOptions = exactNameGroup ? [ exactNameGroup , ... filteredOptions . filter ( ( { option : { label } } ) => label !== input ) ] : filteredOptions ; const { getOptionID , inputProps , suggestionProps } = useAutocomplete < AddressesAutocompleteItem > ( { id , options : filteredOptions , onSelect : handleSelect , input , inputRef , } ) ; const handleInputChange = ( newValue ) => { if ( newValue === ';' || newValue === ',' ) { return ; } if ( ! hasEmailPasting ) { setInput ( newValue ) ; return ; } const values = newValue . split ( /[,;]/ ) . map ( ( value ) => value . trim ( ) ) ; if ( values . length > 1 ) { safeAddRecipients ( values . slice ( 0 , - 1 ) . map ( inputToRecipient ) ) ; setInput ( values [ values . length - 1 ] ) ; return ; } setInput ( newValue ) ; } ; return ( < > < Input { ... rest } { ... inputProps } ref = { useCombinedRefs ( ref , inputRef ) } value = { input } onChange = { ( event ) => { handleInputChange ( event . currentTarget . value . trimStart ( ) ) ; onChange ?. ( event ) ; } } onKeyDown = { ( event ) => { setEmailError ( '' ) ; if ( ! inputProps . onKeyDown ( event ) ) { if ( event . key === 'Enter' ) { handleAddRecipientFromInput ( input ) ; event . preventDefault ( ) ; } } onKeyDown ?. ( event ) ; } } onBlur = { ( ) => { inputProps . onBlur ( ) ; if ( hasAddOnBlur ) { handleAddRecipientFromInput ( input ) ; } } } error = { emailError } / > < AutocompleteList anchorRef = { anchorRef } { ... suggestionProps } > { filteredAndSortedOptions . map ( ( { chunks , text , option } , index ) => { return ( < Option key = { option . key } id = { getOptionID ( index ) } title = { option . label } value = { option } disableFocusOnActive onChange = { handleSelect } > { option . type === 'group' ? ( < div className = "flex flex-nowrap flex-flex-align-items-center" > < Icon name = "circle-filled" color = { option . value . Color } size = { 12 } className = "mr0-5 flex-item-noshrink flex-item-centered-vert" / > < span className = "mr0-5 text-ellipsis" > < Marks chunks = { chunks } > { text } < /Marks> < /span> { getNumberOfMembersText ( option . value . ID , groupsWithContactsMap ) } < /div>) : (<Marks chunks={chunks}>{text}</Marks > ) } < /Option>); } ) } < /AutocompleteList> < />); } ) ; export default AddressesAutocomplete ;