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 await listInvitations(abortController.signal, share.shareId).then((invites: ShareInvitation[]) => {
72 setInvitations(invites);
76 await listExternalInvitations(abortController.signal, share.shareId).then(
77 (externalInvites: ShareExternalInvitation[]) => {
78 if (externalInvites) {
79 setExternalInvitations(externalInvites);
84 await getShareMembers(abortController.signal, { shareId: share.shareId }).then((members) => {
89 setVolumeId(share.volumeId);
93 abortController.abort();
95 }, [rootShareId, linkId, volumeId]);
97 const updateIsSharedStatus = async (abortSignal: AbortSignal) => {
98 const updatedLink = await getLink(abortSignal, rootShareId, linkId);
99 setIsShared(updatedLink.isShared);
102 const deleteShareIfEmpty = useCallback(
107 updatedMembers?: ShareMember[];
108 updatedInvitations?: ShareInvitation[];
110 const membersCompare = updatedMembers || members;
111 const invitationCompare = updatedInvitations || invitations;
112 if (membersCompare.length || invitationCompare.length) {
116 const abortController = new AbortController();
117 const link = await getLink(abortController.signal, rootShareId, linkId);
118 if (!link.shareId || link.shareUrl) {
122 await deleteShare(link.shareId, { silence: true });
123 await updateIsSharedStatus(abortController.signal);
128 [members, invitations, rootShareId]
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');
137 return link.sharingDetails.shareId;
140 const updateStoredMembers = async (memberId: string, member?: ShareMember | undefined) => {
141 const updatedMembers = members.reduce<ShareMember[]>((acc, item) => {
142 if (item.memberId === memberId) {
146 return [...acc, member];
148 return [...acc, item];
150 if (updatedMembers) {
151 await deleteShareIfEmpty({ updatedMembers });
153 setMembers(updatedMembers);
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),
161 setVolumeId(share.volumeId);
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 };
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;
177 const addNewMember = async ({
182 invitee: ShareInvitee;
183 permissions: SHARE_MEMBER_PERMISSIONS;
184 emailDetails?: ShareInvitationEmailDetails;
186 externalInvitation?: ShareExternalInvitation;
187 invitation?: ShareInvitation;
190 const abortSignal = new AbortController().signal;
193 shareId: linkShareId,
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');
203 if (!invitee.publicKey) {
204 return inviteExternalUser(abortSignal, {
206 shareId: linkShareId,
208 inviteeEmail: invitee.email,
210 inviterEmail: primaryAddressKey.address.Email,
211 addressKey: primaryAddressKey.privateKey,
219 return inviteProtonUser(abortSignal, {
221 shareId: linkShareId,
225 inviteeEmail: invitee.email,
226 publicKey: invitee.publicKey,
229 inviterEmail: primaryAddressKey.address.Email,
230 addressKey: primaryAddressKey.privateKey,
237 const addNewMembers = async ({
242 invitees: ShareInvitee[];
243 permissions: SHARE_MEMBER_PERMISSIONS;
244 emailDetails?: ShareInvitationEmailDetails;
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 }) => {
254 newInvitations.push(invitation);
255 } else if (externalInvitation) {
256 newExternalInvitations.push(externalInvitation);
261 await updateIsSharedStatus(abortController.signal);
262 setInvitations((oldInvitations: ShareInvitation[]) => [...oldInvitations, ...newInvitations]);
263 setExternalInvitations((oldExternalInvitations: ShareExternalInvitation[]) => [
264 ...oldExternalInvitations,
265 ...newExternalInvitations,
267 createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
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` });
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` });
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 });
298 setInvitations(updatedInvitations);
299 createNotification({ type: 'info', text: c('Notification').t`Access updated` });
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` });
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` });
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)
326 createNotification({ type: 'info', text: c('Notification').t`External invitation removed from the share` });
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))
337 createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
340 const updateExternalInvitePermissions = async (
341 externalInvitationId: string,
342 permissions: SHARE_MEMBER_PERMISSIONS
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
353 createNotification({ type: 'info', text: c('Notification').t`Access updated and shared` });
366 removeExternalInvitation,
371 resendExternalInvitation,
372 updateMemberPermissions,
373 updateInvitePermissions,
374 updateExternalInvitePermissions,
379 export default useShareMemberView;