Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _views / useShareMemberView.tsx
blobf37facb8f032962acdb32ce6c4cc4911a072a495
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             await listInvitations(abortController.signal, share.shareId).then((invites: ShareInvitation[]) => {
71                 if (invites) {
72                     setInvitations(invites);
73                 }
74             });
76             await listExternalInvitations(abortController.signal, share.shareId).then(
77                 (externalInvites: ShareExternalInvitation[]) => {
78                     if (externalInvites) {
79                         setExternalInvitations(externalInvites);
80                     }
81                 }
82             );
84             await getShareMembers(abortController.signal, { shareId: share.shareId }).then((members) => {
85                 if (members) {
86                     setMembers(members);
87                 }
88             });
89             setVolumeId(share.volumeId);
90         });
92         return () => {
93             abortController.abort();
94         };
95     }, [rootShareId, linkId, volumeId]);
97     const updateIsSharedStatus = async (abortSignal: AbortSignal) => {
98         const updatedLink = await getLink(abortSignal, rootShareId, linkId);
99         setIsShared(updatedLink.isShared);
100     };
102     const deleteShareIfEmpty = useCallback(
103         async ({
104             updatedMembers,
105             updatedInvitations,
106         }: {
107             updatedMembers?: ShareMember[];
108             updatedInvitations?: ShareInvitation[];
109         } = {}) => {
110             const membersCompare = updatedMembers || members;
111             const invitationCompare = updatedInvitations || invitations;
112             if (membersCompare.length || invitationCompare.length) {
113                 return;
114             }
116             const abortController = new AbortController();
117             const link = await getLink(abortController.signal, rootShareId, linkId);
118             if (!link.shareId || link.shareUrl) {
119                 return;
120             }
121             try {
122                 await deleteShare(link.shareId, { silence: true });
123                 await updateIsSharedStatus(abortController.signal);
124             } catch (e) {
125                 return;
126             }
127         },
128         [members, invitations, rootShareId]
129     );
131     const getShareId = async (abortSignal: AbortSignal): Promise<string> => {
132         const link = await getLink(abortSignal, rootShareId, linkId);
133         // This should not happen - TS gymnastics
134         if (!link.sharingDetails) {
135             throw new Error('No details for sharing link');
136         }
137         return link.sharingDetails.shareId;
138     };
140     const updateStoredMembers = async (memberId: string, member?: ShareMember | undefined) => {
141         const updatedMembers = members.reduce<ShareMember[]>((acc, item) => {
142             if (item.memberId === memberId) {
143                 if (!member) {
144                     return acc;
145                 }
146                 return [...acc, member];
147             }
148             return [...acc, item];
149         }, []);
150         if (updatedMembers) {
151             await deleteShareIfEmpty({ updatedMembers });
152         }
153         setMembers(updatedMembers);
154     };
156     const getShareIdWithSessionkey = async (abortSignal: AbortSignal, rootShareId: string, linkId: string) => {
157         const [share, link] = await Promise.all([
158             getShareWithKey(abortSignal, rootShareId),
159             getLink(abortSignal, rootShareId, linkId),
160         ]);
161         setVolumeId(share.volumeId);
162         if (link.shareId) {
163             const linkPrivateKey = await getLinkPrivateKey(abortSignal, rootShareId, linkId);
165             const sessionKey = await getShareSessionKey(abortSignal, link.shareId, linkPrivateKey);
166             return { shareId: link.shareId, sessionKey, addressId: share.addressId };
167         }
169         const createShareResult = await createShare(abortSignal, rootShareId, share.volumeId, linkId);
170         // TODO: Volume event is not properly handled for share creation, we load fresh link for now
171         await events.pollEvents.volumes(share.volumeId);
172         await loadFreshLink(abortSignal, rootShareId, linkId);
174         return createShareResult;
175     };
177     const addNewMember = async ({
178         invitee,
179         permissions,
180         emailDetails,
181     }: {
182         invitee: ShareInvitee;
183         permissions: SHARE_MEMBER_PERMISSIONS;
184         emailDetails?: ShareInvitationEmailDetails;
185     }): Promise<{
186         externalInvitation?: ShareExternalInvitation;
187         invitation?: ShareInvitation;
188         code: number;
189     }> => {
190         const abortSignal = new AbortController().signal;
192         const {
193             shareId: linkShareId,
194             sessionKey,
195             addressId,
196         } = await getShareIdWithSessionkey(abortSignal, rootShareId, linkId);
197         const primaryAddressKey = await getShareCreatorKeys(abortSignal, rootShareId);
199         if (!primaryAddressKey) {
200             throw new Error('Could not find primary address key for share owner');
201         }
203         if (!invitee.publicKey) {
204             return inviteExternalUser(abortSignal, {
205                 rootShareId,
206                 shareId: linkShareId,
207                 linkId,
208                 inviteeEmail: invitee.email,
209                 inviter: {
210                     inviterEmail: primaryAddressKey.address.Email,
211                     addressKey: primaryAddressKey.privateKey,
212                     addressId,
213                 },
214                 permissions,
215                 emailDetails,
216             });
217         }
219         return inviteProtonUser(abortSignal, {
220             share: {
221                 shareId: linkShareId,
222                 sessionKey,
223             },
224             invitee: {
225                 inviteeEmail: invitee.email,
226                 publicKey: invitee.publicKey,
227             },
228             inviter: {
229                 inviterEmail: primaryAddressKey.address.Email,
230                 addressKey: primaryAddressKey.privateKey,
231             },
232             emailDetails,
233             permissions,
234         });
235     };
237     const addNewMembers = async ({
238         invitees,
239         permissions,
240         emailDetails,
241     }: {
242         invitees: ShareInvitee[];
243         permissions: SHARE_MEMBER_PERMISSIONS;
244         emailDetails?: ShareInvitationEmailDetails;
245     }) => {
246         await withAdding(async () => {
247             const abortController = new AbortController();
248             const newInvitations: ShareInvitation[] = [];
249             const newExternalInvitations: ShareExternalInvitation[] = [];
250             for (let invitee of invitees) {
251                 await addNewMember({ invitee, permissions, emailDetails }).then(
252                     ({ invitation, externalInvitation }) => {
253                         if (invitation) {
254                             newInvitations.push(invitation);
255                         } else if (externalInvitation) {
256                             newExternalInvitations.push(externalInvitation);
257                         }
258                     }
259                 );
260             }
261             await updateIsSharedStatus(abortController.signal);
262             setInvitations((oldInvitations: ShareInvitation[]) => [...oldInvitations, ...newInvitations]);
263             setExternalInvitations((oldExternalInvitations: ShareExternalInvitation[]) => [
264                 ...oldExternalInvitations,
265                 ...newExternalInvitations,
266             ]);
267             createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
268         });
269     };
271     const updateMemberPermissions = async (member: ShareMember) => {
272         const abortSignal = new AbortController().signal;
273         const shareId = await getShareId(abortSignal);
275         await updateShareMemberPermissions(abortSignal, { shareId, member });
276         await updateStoredMembers(member.memberId, member);
277         createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
278     };
280     const removeMember = async (member: ShareMember) => {
281         const abortSignal = new AbortController().signal;
282         const shareId = await getShareId(abortSignal);
284         await removeShareMember(abortSignal, { shareId, memberId: member.memberId });
285         await updateStoredMembers(member.memberId);
286         createNotification({ type: 'info', text: c('Notification').t`Access for the member removed` });
287     };
289     const removeInvitation = async (invitationId: string) => {
290         const abortSignal = new AbortController().signal;
291         const shareId = await getShareId(abortSignal);
293         await deleteInvitation(abortSignal, { shareId, invitationId });
294         const updatedInvitations = invitations.filter((item) => item.invitationId !== invitationId);
295         if (updatedInvitations.length === 0) {
296             await deleteShareIfEmpty({ updatedInvitations });
297         }
298         setInvitations(updatedInvitations);
299         createNotification({ type: 'info', text: c('Notification').t`Access updated` });
300     };
302     const resendInvitation = async (invitationId: string) => {
303         const abortSignal = new AbortController().signal;
304         const shareId = await getShareId(abortSignal);
306         await resendInvitationEmail(abortSignal, { shareId, invitationId });
307         createNotification({ type: 'info', text: c('Notification').t`Invitation's email was sent again` });
308     };
310     const resendExternalInvitation = async (externalInvitationId: string) => {
311         const abortSignal = new AbortController().signal;
312         const shareId = await getShareId(abortSignal);
314         await resendExternalInvitationEmail(abortSignal, { shareId, externalInvitationId });
315         createNotification({ type: 'info', text: c('Notification').t`External invitation's email was sent again` });
316     };
318     const removeExternalInvitation = async (externalInvitationId: string) => {
319         const abortSignal = new AbortController().signal;
320         const shareId = await getShareId(abortSignal);
322         await deleteExternalInvitation(abortSignal, { shareId, externalInvitationId });
323         setExternalInvitations((current) =>
324             current.filter((item) => item.externalInvitationId !== externalInvitationId)
325         );
326         createNotification({ type: 'info', text: c('Notification').t`External invitation removed from the share` });
327     };
329     const updateInvitePermissions = async (invitationId: string, permissions: SHARE_MEMBER_PERMISSIONS) => {
330         const abortSignal = new AbortController().signal;
331         const shareId = await getShareId(abortSignal);
333         await updateInvitationPermissions(abortSignal, { shareId, invitationId, permissions });
334         setInvitations((current) =>
335             current.map((item) => (item.invitationId === invitationId ? { ...item, permissions } : item))
336         );
337         createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
338     };
340     const updateExternalInvitePermissions = async (
341         externalInvitationId: string,
342         permissions: SHARE_MEMBER_PERMISSIONS
343     ) => {
344         const abortSignal = new AbortController().signal;
345         const shareId = await getShareId(abortSignal);
347         await updateExternalInvitationPermissions(abortSignal, { shareId, externalInvitationId, permissions });
348         setExternalInvitations((current) =>
349             current.map((item) =>
350                 item.externalInvitationId === externalInvitationId ? { ...item, permissions } : item
351             )
352         );
353         createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
354     };
356     return {
357         volumeId,
358         members,
359         invitations,
360         externalInvitations,
361         existingEmails,
362         isShared,
363         isLoading,
364         isAdding,
365         removeInvitation,
366         removeExternalInvitation,
367         removeMember,
368         addNewMember,
369         addNewMembers,
370         resendInvitation,
371         resendExternalInvitation,
372         updateMemberPermissions,
373         updateInvitePermissions,
374         updateExternalInvitePermissions,
375         deleteShareIfEmpty,
376     };
379 export default useShareMemberView;