Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _views / useShareMemberView.tsx
blob77c2ed5e1790c31d65b51656b98fe51e4208e9f1
1 import { useCallback, useEffect, useMemo, useState } from 'react';
3 import { c } from 'ttag';
5 import { useNotifications } from '@proton/components';
6 import { useLoading } from '@proton/hooks';
7 import type { SHARE_MEMBER_PERMISSIONS } from '@proton/shared/lib/drive/permissions';
9 import { useDriveEventManager } from '..';
10 import { useInvitations } from '../_invitations';
11 import { useLink } from '../_links';
12 import type {
13     ShareExternalInvitation,
14     ShareInvitation,
15     ShareInvitationEmailDetails,
16     ShareInvitee,
17     ShareMember,
18 } from '../_shares';
19 import { useShare, useShareActions, useShareMember } from '../_shares';
21 const useShareMemberView = (rootShareId: string, linkId: string) => {
22     const {
23         inviteProtonUser,
24         inviteExternalUser,
25         resendInvitationEmail,
26         resendExternalInvitationEmail,
27         listInvitations,
28         listExternalInvitations,
29         deleteInvitation,
30         deleteExternalInvitation,
31         updateInvitationPermissions,
32         updateExternalInvitationPermissions,
33     } = useInvitations();
34     const { updateShareMemberPermissions, getShareMembers, removeShareMember } = useShareMember();
35     const { getLink, getLinkPrivateKey, loadFreshLink } = useLink();
36     const { createNotification } = useNotifications();
37     const [isLoading, withLoading] = useLoading();
38     const [isAdding, withAdding] = useLoading();
39     const { getShare, getShareWithKey, getShareSessionKey, getShareCreatorKeys } = useShare();
40     const [members, setMembers] = useState<ShareMember[]>([]);
41     const [invitations, setInvitations] = useState<ShareInvitation[]>([]);
42     const [externalInvitations, setExternalInvitations] = useState<ShareExternalInvitation[]>([]);
43     const { createShare, deleteShare } = useShareActions();
44     const events = useDriveEventManager();
45     const [volumeId, setVolumeId] = useState<string>();
46     const [isShared, setIsShared] = useState<boolean>(false);
48     const existingEmails = useMemo(() => {
49         const membersEmail = members.map((member) => member.email);
50         const invitationsEmail = invitations.map((invitation) => invitation.inviteeEmail);
51         const externalInvitationsEmail = externalInvitations.map(
52             (externalInvitation) => externalInvitation.inviteeEmail
53         );
54         return [...membersEmail, ...invitationsEmail, ...externalInvitationsEmail];
55     }, [members, invitations, externalInvitations]);
57     useEffect(() => {
58         const abortController = new AbortController();
59         if (volumeId || isLoading) {
60             return;
61         }
62         void withLoading(async () => {
63             const link = await getLink(abortController.signal, rootShareId, linkId);
64             if (!link.shareId) {
65                 return;
66             }
67             setIsShared(link.isShared);
68             const share = await getShare(abortController.signal, link.shareId);
70             const [fetchedInvitations, fetchedExternalInvitations, fetchedMembers] = await Promise.all([
71                 listInvitations(abortController.signal, share.shareId),
72                 listExternalInvitations(abortController.signal, share.shareId),
73                 getShareMembers(abortController.signal, { shareId: share.shareId }),
74             ]);
76             if (fetchedInvitations) {
77                 setInvitations(fetchedInvitations);
78             }
79             if (fetchedExternalInvitations) {
80                 setExternalInvitations(fetchedExternalInvitations);
81             }
82             if (fetchedMembers) {
83                 setMembers(fetchedMembers);
84             }
86             setVolumeId(share.volumeId);
87         });
89         return () => {
90             abortController.abort();
91         };
92     }, [rootShareId, linkId, volumeId]);
94     const updateIsSharedStatus = async (abortSignal: AbortSignal) => {
95         const updatedLink = await getLink(abortSignal, rootShareId, linkId);
96         setIsShared(updatedLink.isShared);
97     };
99     const deleteShareIfEmpty = useCallback(
100         async ({
101             updatedMembers,
102             updatedInvitations,
103         }: {
104             updatedMembers?: ShareMember[];
105             updatedInvitations?: ShareInvitation[];
106         } = {}) => {
107             const membersCompare = updatedMembers || members;
108             const invitationCompare = updatedInvitations || invitations;
109             if (membersCompare.length || invitationCompare.length) {
110                 return;
111             }
113             const abortController = new AbortController();
114             const link = await getLink(abortController.signal, rootShareId, linkId);
115             if (!link.shareId || link.shareUrl) {
116                 return;
117             }
118             try {
119                 await deleteShare(link.shareId, { silence: true });
120                 await updateIsSharedStatus(abortController.signal);
121             } catch (e) {
122                 return;
123             }
124         },
125         [members, invitations, rootShareId]
126     );
128     const getShareId = async (abortSignal: AbortSignal): Promise<string> => {
129         const link = await getLink(abortSignal, rootShareId, linkId);
130         // This should not happen - TS gymnastics
131         if (!link.sharingDetails) {
132             throw new Error('No details for sharing link');
133         }
134         return link.sharingDetails.shareId;
135     };
137     const updateStoredMembers = async (memberId: string, member?: ShareMember | undefined) => {
138         const updatedMembers = members.reduce<ShareMember[]>((acc, item) => {
139             if (item.memberId === memberId) {
140                 if (!member) {
141                     return acc;
142                 }
143                 return [...acc, member];
144             }
145             return [...acc, item];
146         }, []);
147         if (updatedMembers) {
148             await deleteShareIfEmpty({ updatedMembers });
149         }
150         setMembers(updatedMembers);
151     };
153     const getShareIdWithSessionkey = async (abortSignal: AbortSignal, rootShareId: string, linkId: string) => {
154         const [share, link] = await Promise.all([
155             getShareWithKey(abortSignal, rootShareId),
156             getLink(abortSignal, rootShareId, linkId),
157         ]);
158         setVolumeId(share.volumeId);
159         if (link.shareId) {
160             const linkPrivateKey = await getLinkPrivateKey(abortSignal, rootShareId, linkId);
162             const sessionKey = await getShareSessionKey(abortSignal, link.shareId, linkPrivateKey);
163             return { shareId: link.shareId, sessionKey, addressId: share.addressId };
164         }
166         const createShareResult = await createShare(abortSignal, rootShareId, share.volumeId, linkId);
167         // TODO: Volume event is not properly handled for share creation, we load fresh link for now
168         await events.pollEvents.volumes(share.volumeId);
169         await loadFreshLink(abortSignal, rootShareId, linkId);
171         return createShareResult;
172     };
174     const addNewMember = async ({
175         invitee,
176         permissions,
177         emailDetails,
178     }: {
179         invitee: ShareInvitee;
180         permissions: SHARE_MEMBER_PERMISSIONS;
181         emailDetails?: ShareInvitationEmailDetails;
182     }): Promise<{
183         externalInvitation?: ShareExternalInvitation;
184         invitation?: ShareInvitation;
185         code: number;
186     }> => {
187         const abortSignal = new AbortController().signal;
189         const {
190             shareId: linkShareId,
191             sessionKey,
192             addressId,
193         } = await getShareIdWithSessionkey(abortSignal, rootShareId, linkId);
194         const primaryAddressKey = await getShareCreatorKeys(abortSignal, rootShareId);
196         if (!primaryAddressKey) {
197             throw new Error('Could not find primary address key for share owner');
198         }
200         if (!invitee.publicKey) {
201             return inviteExternalUser(abortSignal, {
202                 rootShareId,
203                 shareId: linkShareId,
204                 linkId,
205                 inviteeEmail: invitee.email,
206                 inviter: {
207                     inviterEmail: primaryAddressKey.address.Email,
208                     addressKey: primaryAddressKey.privateKey,
209                     addressId,
210                 },
211                 permissions,
212                 emailDetails,
213             });
214         }
216         return inviteProtonUser(abortSignal, {
217             share: {
218                 shareId: linkShareId,
219                 sessionKey,
220             },
221             invitee: {
222                 inviteeEmail: invitee.email,
223                 publicKey: invitee.publicKey,
224             },
225             inviter: {
226                 inviterEmail: primaryAddressKey.address.Email,
227                 addressKey: primaryAddressKey.privateKey,
228             },
229             emailDetails,
230             permissions,
231         });
232     };
234     const addNewMembers = async ({
235         invitees,
236         permissions,
237         emailDetails,
238     }: {
239         invitees: ShareInvitee[];
240         permissions: SHARE_MEMBER_PERMISSIONS;
241         emailDetails?: ShareInvitationEmailDetails;
242     }) => {
243         await withAdding(async () => {
244             const abortController = new AbortController();
245             const newInvitations: ShareInvitation[] = [];
246             const newExternalInvitations: ShareExternalInvitation[] = [];
247             for (let invitee of invitees) {
248                 await addNewMember({ invitee, permissions, emailDetails }).then(
249                     ({ invitation, externalInvitation }) => {
250                         if (invitation) {
251                             newInvitations.push(invitation);
252                         } else if (externalInvitation) {
253                             newExternalInvitations.push(externalInvitation);
254                         }
255                     }
256                 );
257             }
258             await updateIsSharedStatus(abortController.signal);
259             setInvitations((oldInvitations: ShareInvitation[]) => [...oldInvitations, ...newInvitations]);
260             setExternalInvitations((oldExternalInvitations: ShareExternalInvitation[]) => [
261                 ...oldExternalInvitations,
262                 ...newExternalInvitations,
263             ]);
264             createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
265         });
266     };
268     const updateMemberPermissions = async (member: ShareMember) => {
269         const abortSignal = new AbortController().signal;
270         const shareId = await getShareId(abortSignal);
272         await updateShareMemberPermissions(abortSignal, { shareId, member });
273         await updateStoredMembers(member.memberId, member);
274         createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
275     };
277     const removeMember = async (member: ShareMember) => {
278         const abortSignal = new AbortController().signal;
279         const shareId = await getShareId(abortSignal);
281         await removeShareMember(abortSignal, { shareId, memberId: member.memberId });
282         await updateStoredMembers(member.memberId);
283         createNotification({ type: 'info', text: c('Notification').t`Access for the member removed` });
284     };
286     const removeInvitation = async (invitationId: string) => {
287         const abortSignal = new AbortController().signal;
288         const shareId = await getShareId(abortSignal);
290         await deleteInvitation(abortSignal, { shareId, invitationId });
291         const updatedInvitations = invitations.filter((item) => item.invitationId !== invitationId);
292         if (updatedInvitations.length === 0) {
293             await deleteShareIfEmpty({ updatedInvitations });
294         }
295         setInvitations(updatedInvitations);
296         createNotification({ type: 'info', text: c('Notification').t`Access updated` });
297     };
299     const resendInvitation = async (invitationId: string) => {
300         const abortSignal = new AbortController().signal;
301         const shareId = await getShareId(abortSignal);
303         await resendInvitationEmail(abortSignal, { shareId, invitationId });
304         createNotification({ type: 'info', text: c('Notification').t`Invitation's email was sent again` });
305     };
307     const resendExternalInvitation = async (externalInvitationId: string) => {
308         const abortSignal = new AbortController().signal;
309         const shareId = await getShareId(abortSignal);
311         await resendExternalInvitationEmail(abortSignal, { shareId, externalInvitationId });
312         createNotification({ type: 'info', text: c('Notification').t`External invitation's email was sent again` });
313     };
315     const removeExternalInvitation = async (externalInvitationId: string) => {
316         const abortSignal = new AbortController().signal;
317         const shareId = await getShareId(abortSignal);
319         await deleteExternalInvitation(abortSignal, { shareId, externalInvitationId });
320         setExternalInvitations((current) =>
321             current.filter((item) => item.externalInvitationId !== externalInvitationId)
322         );
323         createNotification({ type: 'info', text: c('Notification').t`External invitation removed from the share` });
324     };
326     const updateInvitePermissions = async (invitationId: string, permissions: SHARE_MEMBER_PERMISSIONS) => {
327         const abortSignal = new AbortController().signal;
328         const shareId = await getShareId(abortSignal);
330         await updateInvitationPermissions(abortSignal, { shareId, invitationId, permissions });
331         setInvitations((current) =>
332             current.map((item) => (item.invitationId === invitationId ? { ...item, permissions } : item))
333         );
334         createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
335     };
337     const updateExternalInvitePermissions = async (
338         externalInvitationId: string,
339         permissions: SHARE_MEMBER_PERMISSIONS
340     ) => {
341         const abortSignal = new AbortController().signal;
342         const shareId = await getShareId(abortSignal);
344         await updateExternalInvitationPermissions(abortSignal, { shareId, externalInvitationId, permissions });
345         setExternalInvitations((current) =>
346             current.map((item) =>
347                 item.externalInvitationId === externalInvitationId ? { ...item, permissions } : item
348             )
349         );
350         createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
351     };
353     return {
354         volumeId,
355         members,
356         invitations,
357         externalInvitations,
358         existingEmails,
359         isShared,
360         isLoading,
361         isAdding,
362         removeInvitation,
363         removeExternalInvitation,
364         removeMember,
365         addNewMember,
366         addNewMembers,
367         resendInvitation,
368         resendExternalInvitation,
369         updateMemberPermissions,
370         updateInvitePermissions,
371         updateExternalInvitePermissions,
372         deleteShareIfEmpty,
373     };
376 export default useShareMemberView;