Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / contacts / selector / ContactSelectorModal.tsx
blob3ab80dead575fb72555a7a5d6c9f4d820067df0f
1 import type { ChangeEvent, FormEvent } from 'react';
2 import { useEffect, useMemo, useRef, useState } from 'react';
4 import { c, msgid } from 'ttag';
6 import { useUserSettings } from '@proton/account/userSettings/hooks';
7 import { Button } from '@proton/atoms';
8 import Form from '@proton/components/components/form/Form';
9 import Checkbox from '@proton/components/components/input/Checkbox';
10 import SearchInput from '@proton/components/components/input/SearchInput';
11 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
12 import ModalTwo from '@proton/components/components/modalTwo/Modal';
13 import ModalTwoContent from '@proton/components/components/modalTwo/ModalContent';
14 import ModalTwoFooter from '@proton/components/components/modalTwo/ModalFooter';
15 import ModalTwoHeader from '@proton/components/components/modalTwo/ModalHeader';
16 import Option from '@proton/components/components/option/Option';
17 import SelectTwo from '@proton/components/components/selectTwo/SelectTwo';
18 import useActiveBreakpoint from '@proton/components/hooks/useActiveBreakpoint';
19 import useContactEmailsSortedByName from '@proton/components/hooks/useContactEmailsSortedByName';
20 import { useContactGroups } from '@proton/mail';
21 import { toMap } from '@proton/shared/lib/helpers/object';
22 import { normalize } from '@proton/shared/lib/helpers/string';
23 import type { Recipient } from '@proton/shared/lib/interfaces/Address';
24 import type { ContactEmail, ContactGroup } from '@proton/shared/lib/interfaces/contacts/Contact';
25 import clsx from '@proton/utils/clsx';
27 import type { ContactEditProps } from '../edit/ContactEditModal';
28 import ContactSelectorEmptyContacts from './ContactSelectorEmptyContacts';
29 import ContactSelectorEmptyResults from './ContactSelectorEmptyResults';
30 import ContactSelectorList from './ContactSelectorList';
31 import ContactSelectorRow from './ContactSelectorRow';
33 import './ContactSelectorModal.scss';
35 const convertContactToRecipient = ({ Name, ContactID, Email }: ContactEmail) => ({
36     Name,
37     ContactID,
38     Address: Email,
39 });
41 export interface ContactSelectorProps {
42     inputValue: any;
43     onGroupDetails: (contactGroupID: string) => void;
44     onEdit: (props: ContactEditProps) => void;
47 interface ContactSelectorResolver {
48     onResolve: (recipients: Recipient[]) => void;
49     onReject: () => void;
52 const allContactsGroup = (): Pick<ContactGroup, 'Name' | 'ID'> => ({
53     ID: 'default',
54     Name: c('Label').t`All contacts`,
55 });
57 type Props = ContactSelectorProps & ContactSelectorResolver & ModalProps;
59 const ContactSelectorModal = ({ onResolve, onReject, inputValue, onGroupDetails, onEdit, ...rest }: Props) => {
60     const { viewportWidth } = useActiveBreakpoint();
62     const searchInputRef = useRef<HTMLInputElement>(null);
63     const [contactEmails, loadingContactEmails] = useContactEmailsSortedByName();
64     const [userSettings, loadingUserSettings] = useUserSettings();
65     const [contactGroups = [], loadingContactGroups] = useContactGroups();
66     const [selectedGroup, setSelectedGroup] = useState<string>(allContactsGroup().ID);
68     const contactGroupsWithDefault = [allContactsGroup(), ...contactGroups];
70     const emailsFromInput = inputValue.map((e: any) => e.Address);
71     const contactGroupsMap = toMap(contactGroups);
73     const initialCheckedContactEmailsMap = contactEmails.reduce(
74         (acc: { [key: string]: boolean }, contactEmail: ContactEmail) => {
75             acc[contactEmail.ID] = emailsFromInput.includes(contactEmail.Email);
76             return acc;
77         },
78         Object.create(null)
79     );
81     const [searchValue, setSearchValue] = useState('');
82     const [lastCheckedID, setLastCheckedID] = useState('');
83     const [isAllChecked, setIsAllChecked] = useState(false);
85     const [filteredContactEmails, setFilteredContactEmails] = useState(contactEmails);
86     const [checkedContactEmailMap, setCheckedContactEmailMap] = useState<{ [key: string]: boolean }>(
87         initialCheckedContactEmailsMap
88     );
89     const [checkedContactEmails, setCheckedContactEmails] = useState<ContactEmail[]>([]);
90     const totalChecked = checkedContactEmails.length;
92     const loading = loadingContactEmails || loadingUserSettings || loadingContactGroups;
94     const toggleCheckAll = (checked: boolean) => {
95         const update = filteredContactEmails.reduce((acc: { [key: string]: boolean }, contactEmail: ContactEmail) => {
96             acc[contactEmail.ID] = checked;
97             return acc;
98         }, Object.create(null));
100         setCheckedContactEmailMap({ ...checkedContactEmailMap, ...update });
101     };
103     const onCheck = (checkedIDs: string[] = [], checked = false) => {
104         const update = checkedIDs.reduce((acc, checkedID) => {
105             acc[checkedID] = checked;
106             return acc;
107         }, Object.create(null));
109         setCheckedContactEmailMap({ ...checkedContactEmailMap, ...update });
110     };
112     const handleCheckAll = (e: ChangeEvent<HTMLInputElement>) => toggleCheckAll(e.target.checked);
114     const handleCheck = (e: ChangeEvent<HTMLInputElement>, checkedID: string) => {
115         const {
116             target,
117             nativeEvent,
118         }: {
119             target: EventTarget & HTMLInputElement;
120             nativeEvent: Event & { shiftKey?: boolean };
121         } = e;
122         const checkedIDs = checkedID ? [checkedID] : [];
124         if (lastCheckedID && nativeEvent.shiftKey) {
125             const start = filteredContactEmails.findIndex((c: ContactEmail) => c.ID === checkedID);
126             const end = filteredContactEmails.findIndex((c: ContactEmail) => c.ID === lastCheckedID);
127             checkedIDs.push(
128                 ...filteredContactEmails
129                     .slice(Math.min(start, end), Math.max(start, end) + 1)
130                     .map((c: ContactEmail) => c.ID)
131             );
132         }
134         if (checkedID) {
135             setLastCheckedID(checkedID);
136             onCheck(checkedIDs, target.checked);
137         }
138     };
140     const handleClearSearch = () => {
141         setSearchValue('');
142         searchInputRef?.current?.focus();
143     };
145     const searchFilter = (c: ContactEmail) => {
146         const tokenizedQuery = normalize(searchValue, true).split(' ');
148         const groupNameTokens = c.LabelIDs.reduce((acc: string[], labelId) => {
149             const tokenized = normalize(contactGroupsMap[labelId].Name, true).split(' ');
150             return [...acc, ...tokenized];
151         }, []);
153         return (
154             tokenizedQuery.some((token) => normalize(c.Name, true).includes(token)) ||
155             tokenizedQuery.some((token) => normalize(c.Email, true).includes(token)) ||
156             tokenizedQuery.some((token) => groupNameTokens.some((g) => g.includes(token)))
157         );
158     };
160     const filterContactsByGroup = useMemo(() => {
161         const filteredContacts = contactEmails;
162         if (selectedGroup === allContactsGroup().ID) {
163             return filteredContacts;
164         }
166         return filteredContacts.filter((contact: ContactEmail) => contact.LabelIDs.includes(selectedGroup));
167     }, [selectedGroup]);
169     useEffect(() => {
170         searchInputRef?.current?.focus();
171     }, []);
173     useEffect(() => {
174         setLastCheckedID('');
175         setFilteredContactEmails(filterContactsByGroup.filter(searchFilter));
176     }, [searchValue, selectedGroup]);
178     useEffect(() => {
179         setCheckedContactEmails(contactEmails.filter((c: ContactEmail) => !!checkedContactEmailMap[c.ID]));
180     }, [checkedContactEmailMap]);
182     useEffect(() => {
183         setIsAllChecked(
184             !!filteredContactEmails.length &&
185                 filteredContactEmails.every((c: ContactEmail) => !!checkedContactEmailMap[c.ID])
186         );
187     }, [filteredContactEmails, checkedContactEmailMap]);
189     const handleSearchValue = (value: string) => setSearchValue(value);
191     const handleSubmit = (event: FormEvent) => {
192         event.stopPropagation();
193         event.preventDefault();
195         onResolve(checkedContactEmails.map(convertContactToRecipient));
196         rest.onClose?.();
197     };
199     const actionText =
200         totalChecked === 1
201             ? c('Action').t`Insert contact`
202             : c('Action').ngettext(
203                   msgid`Insert ${totalChecked} contact`,
204                   `Insert ${totalChecked} contacts`,
205                   totalChecked
206               );
208     return (
209         <ModalTwo size="large" as={Form} onSubmit={handleSubmit} data-testid="modal:contactlist" {...rest}>
210             <ModalTwoHeader title={c('Title').t`Insert contacts`} />
211             <ModalTwoContent>
212                 {!contactEmails.length ? (
213                     <ContactSelectorEmptyContacts onClose={rest.onClose} onEdit={onEdit} />
214                 ) : (
215                     <>
216                         <div
217                             className={clsx(['mb-2 flex flex-nowrap gap-4', viewportWidth['<=small'] && 'flex-column'])}
218                         >
219                             <div className="grow-2">
220                                 <SearchInput
221                                     ref={searchInputRef}
222                                     value={searchValue}
223                                     onChange={handleSearchValue}
224                                     placeholder={c('Placeholder').t`Search name, email or group`}
225                                 />
226                             </div>
227                             <div className={clsx([!viewportWidth['<=small'] && 'w-1/3'])}>
228                                 <SelectTwo
229                                     onChange={({ value }) => setSelectedGroup(value)}
230                                     value={selectedGroup}
231                                     disabled={loadingContactGroups}
232                                 >
233                                     {contactGroupsWithDefault.map((group) => (
234                                         <Option key={group.ID} value={group.ID} title={group.Name}>
235                                             {group.Name}
236                                         </Option>
237                                     ))}
238                                 </SelectTwo>
239                             </div>
240                         </div>
241                         {filteredContactEmails.length ? (
242                             <>
243                                 {!viewportWidth['<=small'] && (
244                                     <div className="flex flex-nowrap flex-1 contact-list-row p-4">
245                                         <div>
246                                             <Checkbox
247                                                 className="w-full h-full"
248                                                 checked={isAllChecked}
249                                                 onChange={handleCheckAll}
250                                             />
251                                         </div>
252                                         <div className="flex flex-1 self-center">
253                                             <div className="w-custom pl-4" style={{ '--w-custom': '45%' }}>
254                                                 <strong className="text-uppercase">{c('Label').t`Name`}</strong>
255                                             </div>
256                                             <div className="flex-1">
257                                                 <strong className="text-uppercase">{c('Label').t`Email`}</strong>
258                                             </div>
259                                         </div>
260                                     </div>
261                                 )}
262                                 <ContactSelectorList
263                                     rowCount={filteredContactEmails.length}
264                                     userSettings={userSettings}
265                                     className={clsx([viewportWidth['<=small'] && 'mt-4'])}
266                                     rowRenderer={({ index, style }) => (
267                                         <ContactSelectorRow
268                                             onCheck={handleCheck}
269                                             style={style}
270                                             key={filteredContactEmails[index].ID}
271                                             contact={filteredContactEmails[index]}
272                                             checked={!!checkedContactEmailMap[filteredContactEmails[index].ID]}
273                                             isSmallViewport={viewportWidth['<=small']}
274                                         />
275                                     )}
276                                 />
277                             </>
278                         ) : (
279                             <ContactSelectorEmptyResults onClearSearch={handleClearSearch} query={searchValue} />
280                         )}
281                     </>
282                 )}
283             </ModalTwoContent>
284             <ModalTwoFooter>
285                 <Button type="button" onClick={rest.onClose} disabled={loading}>
286                     {c('Action').t`Cancel`}
287                 </Button>
288                 {contactEmails.length ? (
289                     <Button
290                         color="norm"
291                         loading={loading}
292                         type="submit"
293                         disabled={!totalChecked}
294                         data-testid="modal:contactlist:submit"
295                     >
296                         {actionText}
297                     </Button>
298                 ) : null}
299             </ModalTwoFooter>
300         </ModalTwo>
301     );
304 export default ContactSelectorModal;