Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / contacts / widget / ContactsWidgetGroupsContainer.tsx
blob938c00231ae1fb911a1fcd0655f5bdb2c84994f6
1 import { useMemo, useState } from 'react';
3 import { c, msgid } from 'ttag';
5 import { useUser } from '@proton/account/user/hooks';
6 import { CircleLoader } from '@proton/atoms';
7 import SearchInput from '@proton/components/components/input/SearchInput';
8 import useNotifications from '@proton/components/hooks/useNotifications';
9 import { useContactGroups } from '@proton/mail';
10 import { useContactEmails } from '@proton/mail/contactEmails/hooks';
11 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
12 import { orderContactGroups } from '@proton/shared/lib/helpers/contactGroups';
13 import { normalize } from '@proton/shared/lib/helpers/string';
14 import type { Recipient } from '@proton/shared/lib/interfaces';
15 import type { ContactEmail } from '@proton/shared/lib/interfaces/contacts';
16 import { DEFAULT_MAILSETTINGS } from '@proton/shared/lib/mail/mailSettings';
18 import useItemsSelection from '../../items/useItemsSelection';
19 import type { ContactGroupDeleteProps } from '../group/ContactGroupDeleteModal';
20 import type { ContactGroupEditProps } from '../group/ContactGroupEditModal';
21 import ContactsGroupsList from '../lists/ContactsGroupsList';
22 import ContactsWidgetGroupsToolbar from './ContactsWidgetGroupsToolbar';
23 import ContactsWidgetPlaceholder, { EmptyType } from './ContactsWidgetPlaceholder';
24 import type { CustomAction } from './types';
26 interface Props {
27     onClose?: () => void;
28     onCompose?: (recipients: Recipient[], attachments: File[]) => void;
29     onImport: () => void;
30     customActions: CustomAction[];
31     onDetails: (contactGroupID: string) => void;
32     onDelete: (props: ContactGroupDeleteProps) => void;
33     onEdit: (props: ContactGroupEditProps) => void;
34     onUpgrade: () => void;
35     isDrawer?: boolean;
38 const ContactsWidgetGroupsContainer = ({
39     onClose,
40     onCompose,
41     onImport,
42     customActions,
43     onDetails,
44     onDelete,
45     onEdit,
46     onUpgrade,
47     isDrawer = false,
48 }: Props) => {
49     const [mailSettings] = useMailSettings();
50     const { createNotification } = useNotifications();
51     const [user] = useUser();
53     const [search, setSearch] = useState('');
55     const [groups = [], loadingGroups] = useContactGroups();
56     const orderedGroups = orderContactGroups(groups);
57     const [contactEmails = [], loadingContactEmails] = useContactEmails();
59     const normalizedSearch = normalize(search);
61     const filteredGroups = useMemo(
62         () => orderedGroups.filter(({ Name }) => normalize(Name).includes(normalizedSearch)),
63         [orderedGroups, normalizedSearch]
64     );
66     const groupIDs = filteredGroups.map((group) => group.ID);
68     const { checkedIDs, selectedIDs, handleCheckAll, handleCheckOne } = useItemsSelection({
69         allIDs: groupIDs,
70     });
72     const allChecked = checkedIDs.length > 0 && checkedIDs.length === filteredGroups.length;
74     const groupsEmailsMap = useMemo(
75         () =>
76             contactEmails.reduce<{ [groupID: string]: ContactEmail[] }>((acc, contactEmail) => {
77                 contactEmail.LabelIDs.forEach((labelID) => {
78                     if (!acc[labelID]) {
79                         acc[labelID] = [];
80                     }
81                     acc[labelID].push(contactEmail);
82                 });
83                 return acc;
84             }, {}),
85         [groups, contactEmails]
86     );
88     const recipients = selectedIDs
89         .map((selectedID) => {
90             const group = groups.find((group) => group.ID === selectedID);
91             if (groupsEmailsMap[selectedID]) {
92                 return groupsEmailsMap[selectedID].map((email) => ({
93                     Name: email.Name,
94                     Address: email.Email,
95                     Group: group?.Path,
96                 }));
97             }
98             return [];
99         })
100         .flat();
102     const handleClearSearch = () => {
103         // If done synchronously, button is removed from the dom and the dropdown considers a click outside
104         setTimeout(() => setSearch(''));
105     };
107     const handleCompose = () => {
108         const maxContacts = mailSettings?.RecipientLimit || DEFAULT_MAILSETTINGS.RecipientLimit;
110         if (recipients.length > maxContacts) {
111             createNotification({
112                 type: 'error',
113                 text: c('Error').ngettext(
114                     msgid`You can't send a mail to more than ${maxContacts} recipient`,
115                     `You can't send a mail to more than ${maxContacts} recipients`,
116                     maxContacts
117                 ),
118             });
119             return;
120         }
122         const noContactGroupIDs = selectedIDs.filter((groupID) => !groupsEmailsMap[groupID]?.length);
124         if (noContactGroupIDs.length) {
125             const noContactsGroupNames = noContactGroupIDs.map(
126                 // Looping in all groups is no really performant but should happen rarely
127                 (groupID) => groups.find((group) => group.ID === groupID)?.Name
128             );
130             const noContactGroupCount = noContactsGroupNames.length;
131             const noContactGroupList = noContactsGroupNames.join(', ');
133             const text = c('Error').ngettext(
134                 msgid`One of the groups has no contacts: ${noContactGroupList}`,
135                 `Some groups have no contacts: ${noContactGroupList} `,
136                 noContactGroupCount
137             );
139             createNotification({ type: 'warning', text });
140         }
142         onCompose?.(recipients, []);
143         onClose?.();
144     };
146     const handleDetails = (groupID: string) => {
147         onDetails(groupID);
148         onClose?.();
149     };
151     const handleDelete = () => {
152         onDelete({
153             groupIDs: selectedIDs,
154             onDelete: () => {
155                 if (selectedIDs.length === filteredGroups.length) {
156                     setSearch('');
157                 }
158                 handleCheckAll(false);
159             },
160         });
161         onClose?.();
162     };
164     const handleCreate = () => {
165         if (!user.hasPaidMail) {
166             onUpgrade();
167             onClose?.();
168             return;
169         }
171         onEdit({});
172         onClose?.();
173     };
175     const handleImport = () => {
176         onImport();
177         onClose?.();
178     };
180     const groupCounts = filteredGroups.length;
182     const loading = loadingGroups || loadingContactEmails;
183     const showPlaceholder = !loading && !groupCounts;
184     const showList = !showPlaceholder;
186     return (
187         <div className="flex flex-column flex-nowrap h-full">
188             <div className="contacts-widget-search-container shrink-0">
189                 <label htmlFor="id_contact-widget-search" className="sr-only">{c('Placeholder')
190                     .t`Search for group name`}</label>
191                 <SearchInput
192                     autoFocus
193                     value={search}
194                     onChange={setSearch}
195                     id="id_contact-widget-group-search"
196                     placeholder={c('Placeholder').t`Group name`}
197                 />
198                 <span className="sr-only" aria-atomic aria-live="assertive">
199                     {c('Info').ngettext(msgid`${groupCounts} group found`, `${groupCounts} groups found`, groupCounts)}
200                 </span>
201             </div>
202             <div className="contacts-widget-toolbar py-4 border-bottom border-weak shrink-0">
203                 <ContactsWidgetGroupsToolbar
204                     allChecked={allChecked}
205                     selected={selectedIDs}
206                     numberOfRecipients={recipients.length}
207                     onCheckAll={handleCheckAll}
208                     onCompose={onCompose ? handleCompose : undefined}
209                     groupsEmailsMap={groupsEmailsMap}
210                     recipients={recipients}
211                     onClose={onClose}
212                     customActions={customActions}
213                     onCreate={handleCreate}
214                     onDelete={handleDelete}
215                     isDrawer={isDrawer}
216                 />
217             </div>
218             <div className="flex-1 w-full">
219                 {loading ? (
220                     <div className="flex h-full">
221                         <CircleLoader className="m-auto color-primary" size="large" />
222                     </div>
223                 ) : null}
224                 {showPlaceholder ? (
225                     <ContactsWidgetPlaceholder
226                         type={groups.length ? EmptyType.Search : EmptyType.AllGroups}
227                         onClearSearch={handleClearSearch}
228                         onCreate={handleCreate}
229                         onImport={handleImport}
230                     />
231                 ) : null}
232                 {showList ? (
233                     <ContactsGroupsList
234                         groups={filteredGroups}
235                         groupsEmailsMap={groupsEmailsMap}
236                         onCheckOne={handleCheckOne}
237                         isLargeViewport={false}
238                         checkedIDs={checkedIDs}
239                         onClick={handleDetails}
240                         isDrawer={isDrawer}
241                         onCompose={onCompose}
242                     />
243                 ) : null}
244             </div>
245         </div>
246     );
249 export default ContactsWidgetGroupsContainer;