1 import { memo, useMemo } from 'react';
3 import { c, msgid } from 'ttag';
5 import { Button } from '@proton/atoms';
7 AddressesAutocompleteTwo,
12 useBusySlotsAvailable,
13 useContactEmailsCache,
14 } from '@proton/components';
15 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
16 import { emailToAttendee } from '@proton/shared/lib/calendar/attendees';
17 import type { VIEWS } from '@proton/shared/lib/calendar/constants';
18 import { ICAL_ATTENDEE_ROLE } from '@proton/shared/lib/calendar/constants';
19 import { getSelfSendAddresses } from '@proton/shared/lib/helpers/address';
23 canonicalizeInternalEmail,
25 } from '@proton/shared/lib/helpers/email';
26 import type { Address, Recipient } from '@proton/shared/lib/interfaces';
27 import type { AttendeeModel, OrganizerModel } from '@proton/shared/lib/interfaces/calendar';
28 import { inputToRecipient } from '@proton/shared/lib/mail/recipient';
29 import uniqueBy from '@proton/utils/uniqueBy';
31 import { selectDisplayAvailabilityUnknown } from '../../../store/busySlots/busySlotsSelectors';
32 import { useCalendarSelector } from '../../../store/hooks';
33 import { getParticipantsError } from '../helpers';
34 import OrganizerRow from '../rows/OrganizerRow';
35 import ParticipantRows from '../rows/ParticipantRows';
37 const { REQUIRED, OPTIONAL } = ICAL_ATTENDEE_ROLE;
40 value: AttendeeModel[];
41 isOwnedCalendar: boolean;
43 organizer?: OrganizerModel;
44 displayBusySlots: boolean;
48 onChange: (recipients: AttendeeModel[]) => void;
49 setParticipantError?: (value: boolean) => void;
50 collapsible?: boolean;
51 onDisplayBusySlots?: () => void;
55 const ParticipantsInput = ({
70 const [mailSettings] = useMailSettings();
71 const isBusySlotsAvailable = useBusySlotsAvailable(view);
72 const displayAvailabilityUnknown = useCalendarSelector(selectDisplayAvailabilityUnknown) && displayBusySlots;
73 const numberOfAttendees = value.length;
75 const { contactEmails, contactGroups, contactEmailsMap, groupsWithContactsMap } = useContactEmailsCache();
77 const ownNormalizedEmails = useMemo(
78 () => getSelfSendAddresses(addresses).map(({ Email }) => canonicalizeInternalEmail(Email)),
82 const recipients = value.map((attendee) => {
83 return inputToRecipient(attendee.email);
86 const recipientsSet = new Set(recipients.map(({ Address }) => canonicalizeEmail(Address)));
88 const error = getParticipantsError({
91 maxAttendees: mailSettings?.RecipientLimit,
94 const handleAddRecipients = (recipients: Recipient[]) => {
95 setParticipantError?.(false);
96 const normalizedRecipients = recipients.map((recipient) => {
97 const { Address } = recipient;
100 normalizedAddress: canonicalizeEmail(Address),
101 valid: validateEmailAddress(Address),
104 const uniqueRecipients = uniqueBy(normalizedRecipients, ({ normalizedAddress }) => normalizedAddress);
105 const newAttendees = uniqueRecipients
106 .filter(({ valid, normalizedAddress }) => {
107 return valid && !recipientsSet.has(normalizedAddress);
109 .map(({ recipient }) => {
110 return emailToAttendee(recipient.Address);
112 if (!newAttendees.length) {
115 const attendees = newAttendees.reduce<AttendeeModel[]>((acc, cur) => {
116 if (!ownNormalizedEmails.includes(canonicalizeEmail(cur.email, CANONICALIZE_SCHEME.PROTON))) {
122 if (attendees.length) {
123 onChange([...attendees, ...value]);
127 const onDelete = (recipient: AttendeeModel) => {
128 onChange(value.filter((item) => recipient !== item));
131 const toggleIsOptional = ({ email }: AttendeeModel) => {
133 value.map((attendee) =>
134 email === attendee.email
135 ? { ...attendee, role: attendee.role === REQUIRED ? OPTIONAL : REQUIRED }
141 const participantRows = (
143 attendeeModel={value}
144 contactEmailsMap={contactEmailsMap}
145 isBusySlotsAvailable={isBusySlotsAvailable && displayBusySlots}
147 toggleIsOptional={toggleIsOptional}
153 <AddressesAutocompleteTwo
155 className={className}
156 placeholder={placeholder}
158 data-testid="participants-input"
159 contactEmails={contactEmails}
160 contactGroups={contactGroups}
161 contactEmailsMap={contactEmailsMap}
162 groupsWithContactsMap={groupsWithContactsMap}
163 recipients={recipients}
164 onAddRecipients={handleAddRecipients}
165 onAddInvalidEmail={() => {
166 setParticipantError?.(true);
168 onChange={(value) => {
169 if (!value.trimStart()) {
170 setParticipantError?.(false);
173 validate={(email) => {
174 if (ownNormalizedEmails.includes(canonicalizeInternalEmail(email))) {
175 return c('Error').t`Self invitation not allowed`;
178 if (!validateEmailAddress(email)) {
179 return c('Error').t`Invalid email address`;
184 <Alert className="mb-4 mt-2" type="error">
188 {numberOfAttendees > 0 &&
190 <Details className="border-none mt-1" open>
192 {c('Event form').ngettext(
193 msgid`${numberOfAttendees} participant`,
194 `${numberOfAttendees} participants`,
203 {numberOfAttendees > 0 && organizer && <OrganizerRow organizer={organizer} />}
204 {isBusySlotsAvailable && displayAvailabilityUnknown && (
206 className="flex items-center color-weak mt-2 text-sm bg-weak rounded py-1 px-2"
207 data-testid="availability-unknown-banner"
209 <Icon name="circle-half-filled" size={2.5} className="rotateZ-45 opacity-70 mr-2" />{' '}
210 {c('Description').t`Availability unknown`}
213 {isBusySlotsAvailable && !!onDisplayBusySlots && (
218 onDisplayBusySlots();
224 {c('Action').t`Show busy times`}
231 export default memo(ParticipantsInput);