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';
28 onCompose?: (recipients: Recipient[], attachments: File[]) => void;
30 customActions: CustomAction[];
31 onDetails: (contactGroupID: string) => void;
32 onDelete: (props: ContactGroupDeleteProps) => void;
33 onEdit: (props: ContactGroupEditProps) => void;
34 onUpgrade: () => void;
38 const ContactsWidgetGroupsContainer = ({
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]
66 const groupIDs = filteredGroups.map((group) => group.ID);
68 const { checkedIDs, selectedIDs, handleCheckAll, handleCheckOne } = useItemsSelection({
72 const allChecked = checkedIDs.length > 0 && checkedIDs.length === filteredGroups.length;
74 const groupsEmailsMap = useMemo(
76 contactEmails.reduce<{ [groupID: string]: ContactEmail[] }>((acc, contactEmail) => {
77 contactEmail.LabelIDs.forEach((labelID) => {
81 acc[labelID].push(contactEmail);
85 [groups, contactEmails]
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) => ({
102 const handleClearSearch = () => {
103 // If done synchronously, button is removed from the dom and the dropdown considers a click outside
104 setTimeout(() => setSearch(''));
107 const handleCompose = () => {
108 const maxContacts = mailSettings?.RecipientLimit || DEFAULT_MAILSETTINGS.RecipientLimit;
110 if (recipients.length > maxContacts) {
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`,
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
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} `,
139 createNotification({ type: 'warning', text });
142 onCompose?.(recipients, []);
146 const handleDetails = (groupID: string) => {
151 const handleDelete = () => {
153 groupIDs: selectedIDs,
155 if (selectedIDs.length === filteredGroups.length) {
158 handleCheckAll(false);
164 const handleCreate = () => {
165 if (!user.hasPaidMail) {
175 const handleImport = () => {
180 const groupCounts = filteredGroups.length;
182 const loading = loadingGroups || loadingContactEmails;
183 const showPlaceholder = !loading && !groupCounts;
184 const showList = !showPlaceholder;
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>
195 id="id_contact-widget-group-search"
196 placeholder={c('Placeholder').t`Group name`}
198 <span className="sr-only" aria-atomic aria-live="assertive">
199 {c('Info').ngettext(msgid`${groupCounts} group found`, `${groupCounts} groups found`, groupCounts)}
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}
212 customActions={customActions}
213 onCreate={handleCreate}
214 onDelete={handleDelete}
218 <div className="flex-1 w-full">
220 <div className="flex h-full">
221 <CircleLoader className="m-auto color-primary" size="large" />
225 <ContactsWidgetPlaceholder
226 type={groups.length ? EmptyType.Search : EmptyType.AllGroups}
227 onClearSearch={handleClearSearch}
228 onCreate={handleCreate}
229 onImport={handleImport}
234 groups={filteredGroups}
235 groupsEmailsMap={groupsEmailsMap}
236 onCheckOne={handleCheckOne}
237 isLargeViewport={false}
238 checkedIDs={checkedIDs}
239 onClick={handleDetails}
241 onCompose={onCompose}
249 export default ContactsWidgetGroupsContainer;