Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / drive / src / app / components / modals / DetailsModal.tsx
blob716810bae2335ad815950b78b7e638b7a73f9bdd
1 import type { ReactNode } from 'react';
2 import { useEffect, useState } from 'react';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import type { ModalStateProps } from '@proton/components';
8 import {
9     Alert,
10     FileNameDisplay,
11     Icon,
12     ModalTwo,
13     ModalTwoContent,
14     ModalTwoFooter,
15     ModalTwoHeader,
16     Row,
17     Tooltip,
18     useModalTwoStatic,
19 } from '@proton/components';
20 import EllipsisLoader from '@proton/components/components/loader/EllipsisLoader';
21 import { useLoading } from '@proton/hooks';
22 import { getNumAccessesTooltipMessage, getSizeTooltipMessage } from '@proton/shared/lib/drive/translations';
23 import humanSize, { bytesSize } from '@proton/shared/lib/helpers/humanSize';
25 import type { DriveFileRevision, SignatureIssues } from '../../store';
26 import { useLinkDetailsView } from '../../store';
27 import type { ParsedExtendedAttributes } from '../../store/_links/extendedAttributes';
28 import useRevisions from '../../store/_revisions/useRevisions';
29 import { formatAccessCount } from '../../utils/formatters';
30 import { Cells } from '../FileBrowser';
31 import SignatureAlert from '../SignatureAlert';
32 import ModalContentLoader from './ModalContentLoader';
34 const { UserNameCell, LocationCell, TimeCell, DescriptiveTypeCell, MimeTypeCell } = Cells;
36 interface Props {
37     shareId: string;
38     linkId: string;
39     onClose?: () => void;
42 interface RowProps {
43     label: React.ReactNode;
44     title?: string;
45     children: ReactNode;
46     dataTestId?: string;
49 interface RevisionDetailsModalProps {
50     shareId: string;
51     linkId: string;
52     revision: DriveFileRevision;
53     name: string;
56 export function RevisionDetailsModal({
57     shareId,
58     linkId,
59     revision,
60     name,
61     onClose,
62     ...modalProps
63 }: RevisionDetailsModalProps & ModalStateProps) {
64     const { getRevisionDecryptedXattrs, checkRevisionSignature } = useRevisions(shareId, linkId);
65     const [xattrs, setXattrs] = useState<ParsedExtendedAttributes>();
66     const [signatureIssues, setSignatureIssues] = useState<SignatureIssues>();
67     const [signatureNetworkError, setSignatureNetworkError] = useState<boolean>(false);
68     const [isLoading, withIsLoading] = useLoading();
69     const [isSignatureLoading, withSignatureLoading] = useLoading();
70     useEffect(() => {
71         const ac = new AbortController();
72         void withIsLoading(
73             getRevisionDecryptedXattrs(ac.signal, revision.xAttr, revision.signatureAddress).then((decryptedXattrs) => {
74                 if (!decryptedXattrs) {
75                     return;
76                 }
77                 setXattrs(decryptedXattrs.xattrs);
78                 if (signatureIssues) {
79                     setSignatureIssues({ ...signatureIssues, ...decryptedXattrs.signatureIssues });
80                 } else {
81                     setSignatureIssues(decryptedXattrs.signatureIssues);
82                 }
83             })
84         );
85         return () => {
86             ac.abort();
87         };
88     }, [revision.xAttr, revision.signatureAddress]);
90     useEffect(() => {
91         const ac = new AbortController();
92         void withSignatureLoading(
93             checkRevisionSignature(ac.signal, revision.id).then((blocksSignatureIssues) => {
94                 if (signatureIssues) {
95                     setSignatureIssues({ ...signatureIssues, ...blocksSignatureIssues });
96                 } else {
97                     setSignatureIssues(blocksSignatureIssues);
98                 }
99             })
100         ).catch(() => {
101             setSignatureNetworkError(true);
102         });
103         return () => {
104             ac.abort();
105         };
106     }, [revision.id]);
107     const renderModalState = () => {
108         return (
109             <ModalTwoContent>
110                 <SignatureAlert
111                     loading={isSignatureLoading}
112                     signatureIssues={signatureIssues}
113                     signatureNetworkError={signatureNetworkError}
114                     signatureAddress={revision.signatureAddress}
115                     isFile
116                     name={name}
117                     className="mb-4"
118                 />
119                 <DetailsRow label={c('Title').t`Name`}>
120                     <FileNameDisplay text={name} />
121                 </DetailsRow>
122                 <DetailsRow label={c('Title').t`Uploaded by`}>
123                     <span className="text-pre">{revision.signatureEmail}</span>
124                 </DetailsRow>
125                 <DetailsRow label={c('Title').t`Uploaded`}>
126                     <TimeCell time={revision.createTime} />
127                 </DetailsRow>
128                 <DetailsRow label={c('Title').t`Modified`}>
129                     {xattrs?.Common.ModificationTime ? (
130                         <TimeCell time={xattrs.Common.ModificationTime} />
131                     ) : isLoading ? (
132                         <EllipsisLoader />
133                     ) : (
134                         '-'
135                     )}
136                 </DetailsRow>
137                 <DetailsRow
138                     label={
139                         <>
140                             {c('Title').t`Size`}
141                             <Tooltip
142                                 title={c('Info')
143                                     .t`The encrypted data is slightly larger due to the overhead of the encryption and signatures, which ensure the security of your data.`}
144                                 className="ml-1 mb-1"
145                             >
146                                 <Icon
147                                     name="info-circle"
148                                     size={3.5}
149                                     alt={c('Info')
150                                         .t`The encrypted data is slightly larger due to the overhead of the encryption and signatures, which ensure the security of your data.`}
151                                 />
152                             </Tooltip>
153                         </>
154                     }
155                 >
156                     <span title={bytesSize(revision.size)}>{humanSize({ bytes: revision.size })}</span>
157                 </DetailsRow>
158                 <DetailsRow label={c('Title').t`Original size`}>
159                     {xattrs?.Common.Size ? (
160                         <span title={bytesSize(xattrs?.Common.Size)}>{humanSize({ bytes: xattrs?.Common.Size })}</span>
161                     ) : isLoading ? (
162                         <EllipsisLoader />
163                     ) : (
164                         '-'
165                     )}
166                 </DetailsRow>
167                 {xattrs?.Common.Digests && (
168                     // This should not be visible in the UI, but needed for e2e
169                     <span data-testid="drive:file-digest" className="hidden" aria-hidden="true">
170                         {xattrs.Common.Digests.SHA1}
171                     </span>
172                 )}
173             </ModalTwoContent>
174         );
175     };
177     return (
178         <ModalTwo onClose={onClose} size="large" {...modalProps}>
179             <ModalTwoHeader title={c('Title').t`Version details`} />
180             {renderModalState()}
181             <ModalTwoFooter>
182                 <Button onClick={onClose}>{c('Action').t`Close`}</Button>
183             </ModalTwoFooter>
184         </ModalTwo>
185     );
188 export default function DetailsModal({ shareId, linkId, onClose, ...modalProps }: Props & ModalStateProps) {
189     const {
190         isLinkLoading,
191         isSignatureIssuesLoading,
192         isNumberOfAccessesLoading,
193         isSharedWithMeLink,
194         error,
195         link,
196         signatureIssues,
197         signatureNetworkError,
198         numberOfAccesses,
199     } = useLinkDetailsView(shareId, linkId);
201     const renderModalState = () => {
202         if (isLinkLoading) {
203             return <ModalContentLoader>{c('Info').t`Loading link`}</ModalContentLoader>;
204         }
206         if (!link || error) {
207             return (
208                 <ModalTwoContent>
209                     <Alert type="error">{c('Info').t`Cannot load link`}</Alert>
210                 </ModalTwoContent>
211             );
212         }
214         return (
215             <ModalTwoContent>
216                 <SignatureAlert
217                     loading={isSignatureIssuesLoading}
218                     signatureIssues={signatureIssues}
219                     signatureNetworkError={signatureNetworkError}
220                     signatureAddress={link.signatureAddress}
221                     corruptedLink={link.corruptedLink}
222                     isFile={link.isFile}
223                     name={link.name}
224                     className="mb-4"
225                 />
226                 <DetailsRow label={c('Title').t`Name`}>
227                     <FileNameDisplay text={link.name} />
228                 </DetailsRow>
229                 {isSharedWithMeLink ? (
230                     <DetailsRow label={c('Title').t`Location`}>
231                         <FileNameDisplay text={`/${c('Info').t`Shared with me`}`} />
232                     </DetailsRow>
233                 ) : (
234                     <DetailsRow label={c('Title').t`Uploaded by`}>
235                         <UserNameCell />
236                     </DetailsRow>
237                 )}
238                 {link.parentLinkId && !isSharedWithMeLink && (
239                     <DetailsRow label={c('Title').t`Location`}>
240                         <LocationCell shareId={shareId} parentLinkId={link.parentLinkId} />
241                     </DetailsRow>
242                 )}
243                 <DetailsRow label={c('Title').t`Uploaded`}>
244                     <TimeCell time={link.createTime} />
245                 </DetailsRow>
246                 <DetailsRow label={c('Title').t`Modified`}>
247                     {link.corruptedLink ? '-' : <TimeCell time={link.fileModifyTime} />}
248                 </DetailsRow>
249                 {link.isFile && (
250                     <>
251                         <DetailsRow label={c('Title').t`Type`}>
252                             <DescriptiveTypeCell mimeType={link.mimeType} isFile={link.isFile} />
253                         </DetailsRow>
254                         <DetailsRow label={c('Title').t`MIME type`}>
255                             <MimeTypeCell mimeType={link.mimeType} />
256                         </DetailsRow>
257                         <DetailsRow
258                             label={
259                                 <>
260                                     {c('Title').t`Size`}
261                                     <Tooltip title={getSizeTooltipMessage()} className="ml-1 mb-1">
262                                         <Icon name="info-circle" size={3.5} alt={getSizeTooltipMessage()} />
263                                     </Tooltip>
264                                 </>
265                             }
266                             dataTestId="file-size"
267                         >
268                             <span title={bytesSize(link.size)}>{humanSize({ bytes: link.size })}</span>
269                         </DetailsRow>
270                         {link.originalSize !== undefined && (
271                             <DetailsRow label={c('Title').t`Original size`}>
272                                 <span title={bytesSize(link.originalSize)}>
273                                     {humanSize({ bytes: link.originalSize })}
274                                 </span>
275                             </DetailsRow>
276                         )}
277                     </>
278                 )}
279                 {link.activeRevision?.signatureAddress && (
280                     <DetailsRow label={c('Title').t`Last edited by`} dataTestId={'drive:last-edited-by'}>
281                         {link.activeRevision?.signatureAddress}
282                     </DetailsRow>
283                 )}
284                 <DetailsRow label={c('Title').t`Shared`} dataTestId={'drive:is-shared'}>
285                     {link.isShared ? c('Info').t`Yes` : c('Info').t`No`}
286                 </DetailsRow>
287                 {link.sharingDetails?.shareUrl && (
288                     <DetailsRow
289                         label={c('Title').t`Public shared link status`}
290                         dataTestId={'drive:public-sharing-status'}
291                     >
292                         {link.sharingDetails.shareUrl.isExpired ? c('Info').t`Expired` : c('Info').t`Available`}
293                     </DetailsRow>
294                 )}
296                 {(numberOfAccesses !== undefined || isNumberOfAccessesLoading) && (
297                     <DetailsRow
298                         label={
299                             <>
300                                 {c('Title').t`# of downloads`}
301                                 <Tooltip title={getNumAccessesTooltipMessage()} className="ml-1 mb-1">
302                                     <Icon name="info-circle" size={3.5} alt={getNumAccessesTooltipMessage()} />
303                                 </Tooltip>
304                             </>
305                         }
306                     >
307                         {formatAccessCount(numberOfAccesses)}
308                     </DetailsRow>
309                 )}
310                 {link.digests && (
311                     // This should not be visible in the UI, but needed for e2e
312                     <span data-testid="drive:file-digest" className="hidden" aria-hidden="true">
313                         {link.digests.sha1}
314                     </span>
315                 )}
316                 {link.activeRevision?.photo?.contentHash && (
317                     // This should not be visible in the UI, but needed for e2e
318                     <span data-testid="drive:photo-contentHash" className="hidden" aria-hidden="true">
319                         {link.activeRevision.photo.contentHash}
320                     </span>
321                 )}
322             </ModalTwoContent>
323         );
324     };
326     return (
327         <ModalTwo onClose={onClose} size="large" {...modalProps}>
328             <ModalTwoHeader title={getTitle(link?.isFile)} />
329             {renderModalState()}
330             <ModalTwoFooter>
331                 <Button onClick={onClose}>{c('Action').t`Close`}</Button>
332             </ModalTwoFooter>
333         </ModalTwo>
334     );
337 function DetailsRow({ label, title, children, dataTestId }: RowProps) {
338     return (
339         <Row title={title}>
340             <span className="label cursor-default">{label}</span>
341             <div className="pt-2" data-testid={dataTestId}>
342                 <b>{children}</b>
343             </div>
344         </Row>
345     );
348 function getTitle(isFile?: boolean) {
349     if (isFile === undefined) {
350         return c('Title').t`Item details`;
351     }
352     return isFile ? c('Title').t`File details` : c('Title').t`Folder details`;
355 export const useDetailsModal = () => {
356     return useModalTwoStatic(DetailsModal);
358 export const useRevisionDetailsModal = () => {
359     return useModalTwoStatic(RevisionDetailsModal);