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';
13 ShareExternalInvitation,
15 ShareInvitationEmailDetails,
19 import { useShare, useShareActions, useShareMember } from '../_shares';
21 const useShareMemberView = (rootShareId: string, linkId: string) => {
25 resendInvitationEmail,
26 resendExternalInvitationEmail,
28 listExternalInvitations,
30 deleteExternalInvitation,
31 updateInvitationPermissions,
32 updateExternalInvitationPermissions,
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
54 return [...membersEmail, ...invitationsEmail, ...externalInvitationsEmail];
55 }, [members, invitations, externalInvitations]);
58 const abortController = new AbortController();
59 if (volumeId || isLoading) {
62 void withLoading(async () => {
63 const link = await getLink(abortController.signal, rootShareId, linkId);
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 }),
76 if (fetchedInvitations) {
77 setInvitations(fetchedInvitations);
79 if (fetchedExternalInvitations) {
80 setExternalInvitations(fetchedExternalInvitations);
83 setMembers(fetchedMembers);
86 setVolumeId(share.volumeId);
90 abortController.abort();
92 }, [rootShareId, linkId, volumeId]);
94 const updateIsSharedStatus = async (abortSignal: AbortSignal) => {
95 const updatedLink = await getLink(abortSignal, rootShareId, linkId);
96 setIsShared(updatedLink.isShared);
99 const deleteShareIfEmpty = useCallback(
104 updatedMembers?: ShareMember[];
105 updatedInvitations?: ShareInvitation[];
107 const membersCompare = updatedMembers || members;
108 const invitationCompare = updatedInvitations || invitations;
109 if (membersCompare.length || invitationCompare.length) {
113 const abortController = new AbortController();
114 const link = await getLink(abortController.signal, rootShareId, linkId);
115 if (!link.shareId || link.shareUrl) {
119 await deleteShare(link.shareId, { silence: true });
120 await updateIsSharedStatus(abortController.signal);
125 [members, invitations, rootShareId]
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');
134 return link.sharingDetails.shareId;
137 const updateStoredMembers = async (memberId: string, member?: ShareMember | undefined) => {
138 const updatedMembers = members.reduce<ShareMember[]>((acc, item) => {
139 if (item.memberId === memberId) {
143 return [...acc, member];
145 return [...acc, item];
147 if (updatedMembers) {
148 await deleteShareIfEmpty({ updatedMembers });
150 setMembers(updatedMembers);
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),
158 setVolumeId(share.volumeId);
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 };
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;
174 const addNewMember = async ({
179 invitee: ShareInvitee;
180 permissions: SHARE_MEMBER_PERMISSIONS;
181 emailDetails?: ShareInvitationEmailDetails;
183 externalInvitation?: ShareExternalInvitation;
184 invitation?: ShareInvitation;
187 const abortSignal = new AbortController().signal;
190 shareId: linkShareId,
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');
200 if (!invitee.publicKey) {
201 return inviteExternalUser(abortSignal, {
203 shareId: linkShareId,
205 inviteeEmail: invitee.email,
207 inviterEmail: primaryAddressKey.address.Email,
208 addressKey: primaryAddressKey.privateKey,
216 return inviteProtonUser(abortSignal, {
218 shareId: linkShareId,
222 inviteeEmail: invitee.email,
223 publicKey: invitee.publicKey,
226 inviterEmail: primaryAddressKey.address.Email,
227 addressKey: primaryAddressKey.privateKey,
234 const addNewMembers = async ({
239 invitees: ShareInvitee[];
240 permissions: SHARE_MEMBER_PERMISSIONS;
241 emailDetails?: ShareInvitationEmailDetails;
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 }) => {
251 newInvitations.push(invitation);
252 } else if (externalInvitation) {
253 newExternalInvitations.push(externalInvitation);
258 await updateIsSharedStatus(abortController.signal);
259 setInvitations((oldInvitations: ShareInvitation[]) => [...oldInvitations, ...newInvitations]);
260 setExternalInvitations((oldExternalInvitations: ShareExternalInvitation[]) => [
261 ...oldExternalInvitations,
262 ...newExternalInvitations,
264 createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
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` });
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` });
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 });
295 setInvitations(updatedInvitations);
296 createNotification({ type: 'info', text: c('Notification').t`Access updated` });
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` });
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` });
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)
323 createNotification({ type: 'info', text: c('Notification').t`External invitation removed from the share` });
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))
334 createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
337 const updateExternalInvitePermissions = async (
338 externalInvitationId: string,
339 permissions: SHARE_MEMBER_PERMISSIONS
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
350 createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
363 removeExternalInvitation,
368 resendExternalInvitation,
369 updateMemberPermissions,
370 updateInvitePermissions,
371 updateExternalInvitePermissions,
376 export default useShareMemberView;