1 import { useCallback, useEffect, useState } from 'react';
3 import { c } from 'ttag';
5 import { useNotifications } from '@proton/components';
6 import { useLoading } from '@proton/hooks';
7 import { SHARE_GENERATED_PASSWORD_LENGTH } from '@proton/shared/lib/drive/constants';
8 import { isProtonDocument } from '@proton/shared/lib/helpers/mimetype';
9 import type { SharedURLSessionKeyPayload } from '@proton/shared/lib/interfaces/drive/sharing';
11 import { sendErrorReport } from '../../utils/errorHandling';
12 import { useDriveDocsPublicSharingFF } from '../_documents';
13 import type { DecryptedLink } from '../_links';
14 import { useLink } from '../_links';
15 import type { ShareURL } from '../_shares';
16 import { getSharedLink, splitGeneratedAndCustomPassword, useShareActions, useShareUrl } from '../_shares';
17 import type useShareMemberView from './useShareMemberView';
19 const getLoadingMessage = (isLinkLoading: boolean, haveShareUrl: boolean, isFile: boolean) => {
21 return c('Info').t`Loading link`;
24 return isFile ? c('Info').t`Preparing link to file` : c('Info').t`Preparing link to folder`;
27 return isFile ? c('Info').t`Creating link to file` : c('Info').t`Creating link to folder`;
30 const getConfirmationMessage = () => {
31 return c('Info').t`This action will delete the link and revoke access for all users.`;
34 const getErrorMessage = (isCreationError: boolean, error: string) => {
35 if (isCreationError) {
37 ? c('Info').t`Failed to generate a secure link. The reason is: ${error}`
38 : c('Info').t`Failed to generate a secure link. Try again later`;
40 return c('Info').t`Failed to open a secure link. The reason is: ${error}`;
43 const getSharingInfoMessage = (isFile: boolean) => {
45 ? c('Info').t`Anyone with this link can access your file.`
46 : c('Info').t`Anyone with this link can access your folder.`;
49 const getPasswordProtectedSharingInfoMessage = (isFile: boolean) => {
51 ? c('Info').t`Only the people with the link and the password can access this file.`
52 : c('Info').t`Only the people with the link and the password can access this folder.`;
56 * useLinkView loads link if not cached yet.
58 export default function useShareURLView(shareId: string, linkId: string) {
59 const { getLink } = useLink();
60 const [shareUrlInfo, setShareUrlInfo] = useState<{
62 keyInfo: SharedURLSessionKeyPayload;
64 const { loadShareUrl, updateShareUrl, deleteShareUrl, createShareUrl, getShareIdWithSessionkey } = useShareUrl();
65 const { deleteShare } = useShareActions();
67 const [sharedLink, setSharedLink] = useState('');
68 const [password, setPassword] = useState('');
69 const [initialExpiration, setInitialExpiration] = useState<number | null>(null);
70 const [error, setError] = useState('');
72 const [link, setLink] = useState<DecryptedLink>();
73 const [isLinkLoading, withLinkLoading] = useLoading(true);
74 const [isShareUrlLoading, withShareUrlLoading] = useLoading(true);
75 const [isSaving, withSaving] = useLoading();
76 const [isDeleting, withDeleting] = useLoading();
77 const [isCreating, withCreating] = useLoading();
78 const { createNotification } = useNotifications();
80 const { isDocsPublicSharingEnabled } = useDriveDocsPublicSharingFF();
82 const shareUrl = shareUrlInfo?.shareUrl;
84 const [, customPassword] = splitGeneratedAndCustomPassword(password, shareUrl);
87 const abortController = new AbortController();
89 getLink(abortController.signal, shareId, linkId)
97 abortController.abort();
99 }, [shareId, linkId]);
101 const ShareID = shareUrl?.shareId;
103 const abortController = new AbortController();
104 void withShareUrlLoading(() => {
106 return Promise.resolve();
109 return loadShareUrl(abortController.signal, shareId, linkId)
110 .then((shareUrlInfo) => {
112 setShareUrlInfo(shareUrlInfo);
113 setPassword(shareUrlInfo.shareUrl.password);
114 setInitialExpiration(shareUrlInfo.shareUrl.expirationTime);
115 const sharedLink = getSharedLink(shareUrlInfo.shareUrl);
117 setSharedLink(sharedLink);
127 abortController.abort();
129 }, [shareId, linkId, ShareID]);
131 const updateLinkState = useCallback(
132 async (abortSignal: AbortSignal) => {
133 const link = await getLink(abortSignal, shareId, linkId);
139 const createSharedLink = () => {
140 void withCreating(async () => {
141 const abortController = new AbortController();
142 const { shareId: linkShareId, sessionKey } = await getShareIdWithSessionkey(
143 abortController.signal,
148 return createShareUrl(abortController.signal, shareId, linkShareId, sessionKey)
149 .then(async (shareUrlInfo) => {
151 setShareUrlInfo(shareUrlInfo);
152 setPassword(shareUrlInfo.shareUrl.password);
153 setInitialExpiration(shareUrlInfo.shareUrl.expirationTime);
154 const sharedLink = getSharedLink(shareUrlInfo.shareUrl);
156 setSharedLink(sharedLink);
157 await updateLinkState(abortController.signal);
167 const saveSharedLink = async (newCustomPassword?: string, newDuration?: number | null) => {
172 // Empty string as a newCustomPassword will remove it from the link.
173 // `undefined` is to leave the password as it is.
174 let newPassword = newCustomPassword;
175 if (newCustomPassword !== undefined && shareUrl.hasGeneratedPasswordIncluded) {
176 newPassword = password.substring(0, SHARE_GENERATED_PASSWORD_LENGTH) + newCustomPassword;
179 const update = () => {
180 const abortController = new AbortController();
181 return updateShareUrl(
182 abortController.signal,
184 shareId: shareUrl.shareId,
185 shareUrlId: shareUrl.shareUrlId,
186 flags: shareUrl.flags,
187 keyInfo: shareUrlInfo.keyInfo,
194 const updatedFields = await withSaving(update()).catch((error) => {
197 text: c('Notification').t`Your settings failed to be saved`,
202 text: c('Notification').t`Your settings have been changed successfully`,
209 keyInfo: shareUrlInfo.keyInfo,
212 if (updatedFields && updatedFields.password !== undefined) {
213 setPassword(updatedFields.password);
215 if (updatedFields && updatedFields.expirationTime !== undefined) {
216 setInitialExpiration(updatedFields.expirationTime);
219 return updatedFields;
222 // TODO: Remove this parameter when we will have share events
223 const deleteLink = async (deleteIfEmptyCallback: ReturnType<typeof useShareMemberView>['deleteShareIfEmpty']) => {
224 if (!link || !shareUrl) {
228 await withDeleting(async () => {
229 const abortController = new AbortController();
230 await deleteShareUrl(shareUrl.shareId, shareUrl.shareUrlId)
232 setShareUrlInfo(undefined);
235 setInitialExpiration(null);
237 text: c('Notification').t`The link to your item was deleted`,
243 text: c('Notification').t`The link to your item failed to be deleted`,
246 await updateLinkState(abortController.signal);
247 await deleteIfEmptyCallback();
251 const stopSharing = async () => {
252 return withDeleting(async () => {
253 const abortController = new AbortController();
254 const loadedLink = await getLink(abortController.signal, shareId, linkId);
255 if (!loadedLink?.sharingDetails) {
258 await deleteShare(loadedLink.sharingDetails.shareId, { force: true })
261 text: c('Notification').t`You stopped sharing this item`,
267 text: c('Notification').t`Stopping the sharing of this item has failed`,
270 await updateLinkState(abortController.signal);
274 const loadingMessage =
275 isLinkLoading || isShareUrlLoading
276 ? getLoadingMessage(isLinkLoading, !!link?.shareUrl, !!link?.isFile)
278 const confirmationMessage = getConfirmationMessage();
279 const haveError = error || (!isLinkLoading && !link);
280 const errorMessage = haveError ? getErrorMessage(!link?.shareUrl, error) : undefined;
281 // Show message "protected by password" only when password is saved.
282 const sharedInfoMessage = customPassword
283 ? getPasswordProtectedSharingInfoMessage(!!link?.isFile)
284 : getSharingInfoMessage(!!link?.isFile);
289 name: link?.name || '', // If the link is not loaded we will return an error message anyway
290 isShareUrlEnabled: !!link?.mimeType && isProtonDocument(link.mimeType) ? isDocsPublicSharingEnabled : true,
294 hasSharedLink: !!link?.shareUrl,
300 hasGeneratedPasswordIncluded: !!shareUrl?.hasGeneratedPasswordIncluded,