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';
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;
43 label: React.ReactNode;
49 interface RevisionDetailsModalProps {
52 revision: DriveFileRevision;
56 export function RevisionDetailsModal({
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();
71 const ac = new AbortController();
73 getRevisionDecryptedXattrs(ac.signal, revision.xAttr, revision.signatureAddress).then((decryptedXattrs) => {
74 if (!decryptedXattrs) {
77 setXattrs(decryptedXattrs.xattrs);
78 if (signatureIssues) {
79 setSignatureIssues({ ...signatureIssues, ...decryptedXattrs.signatureIssues });
81 setSignatureIssues(decryptedXattrs.signatureIssues);
88 }, [revision.xAttr, revision.signatureAddress]);
91 const ac = new AbortController();
92 void withSignatureLoading(
93 checkRevisionSignature(ac.signal, revision.id).then((blocksSignatureIssues) => {
94 if (signatureIssues) {
95 setSignatureIssues({ ...signatureIssues, ...blocksSignatureIssues });
97 setSignatureIssues(blocksSignatureIssues);
101 setSignatureNetworkError(true);
107 const renderModalState = () => {
111 loading={isSignatureLoading}
112 signatureIssues={signatureIssues}
113 signatureNetworkError={signatureNetworkError}
114 signatureAddress={revision.signatureAddress}
119 <DetailsRow label={c('Title').t`Name`}>
120 <FileNameDisplay text={name} />
122 <DetailsRow label={c('Title').t`Uploaded by`}>
123 <span className="text-pre">{revision.signatureEmail}</span>
125 <DetailsRow label={c('Title').t`Uploaded`}>
126 <TimeCell time={revision.createTime} />
128 <DetailsRow label={c('Title').t`Modified`}>
129 {xattrs?.Common.ModificationTime ? (
130 <TimeCell time={xattrs.Common.ModificationTime} />
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"
150 .t`The encrypted data is slightly larger due to the overhead of the encryption and signatures, which ensure the security of your data.`}
156 <span title={bytesSize(revision.size)}>{humanSize({ bytes: revision.size })}</span>
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>
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}
178 <ModalTwo onClose={onClose} size="large" {...modalProps}>
179 <ModalTwoHeader title={c('Title').t`Version details`} />
182 <Button onClick={onClose}>{c('Action').t`Close`}</Button>
188 export default function DetailsModal({ shareId, linkId, onClose, ...modalProps }: Props & ModalStateProps) {
191 isSignatureIssuesLoading,
192 isNumberOfAccessesLoading,
197 signatureNetworkError,
199 } = useLinkDetailsView(shareId, linkId);
201 const renderModalState = () => {
203 return <ModalContentLoader>{c('Info').t`Loading link`}</ModalContentLoader>;
206 if (!link || error) {
209 <Alert type="error">{c('Info').t`Cannot load link`}</Alert>
217 loading={isSignatureIssuesLoading}
218 signatureIssues={signatureIssues}
219 signatureNetworkError={signatureNetworkError}
220 signatureAddress={link.signatureAddress}
221 corruptedLink={link.corruptedLink}
226 <DetailsRow label={c('Title').t`Name`}>
227 <FileNameDisplay text={link.name} />
229 {isSharedWithMeLink ? (
230 <DetailsRow label={c('Title').t`Location`}>
231 <FileNameDisplay text={`/${c('Info').t`Shared with me`}`} />
234 <DetailsRow label={c('Title').t`Uploaded by`}>
238 {link.parentLinkId && !isSharedWithMeLink && (
239 <DetailsRow label={c('Title').t`Location`}>
240 <LocationCell shareId={shareId} parentLinkId={link.parentLinkId} />
243 <DetailsRow label={c('Title').t`Uploaded`}>
244 <TimeCell time={link.createTime} />
246 <DetailsRow label={c('Title').t`Modified`}>
247 {link.corruptedLink ? '-' : <TimeCell time={link.fileModifyTime} />}
251 <DetailsRow label={c('Title').t`Type`}>
252 <DescriptiveTypeCell mimeType={link.mimeType} isFile={link.isFile} />
254 <DetailsRow label={c('Title').t`MIME type`}>
255 <MimeTypeCell mimeType={link.mimeType} />
261 <Tooltip title={getSizeTooltipMessage()} className="ml-1 mb-1">
262 <Icon name="info-circle" size={3.5} alt={getSizeTooltipMessage()} />
266 dataTestId="file-size"
268 <span title={bytesSize(link.size)}>{humanSize({ bytes: link.size })}</span>
270 {link.originalSize !== undefined && (
271 <DetailsRow label={c('Title').t`Original size`}>
272 <span title={bytesSize(link.originalSize)}>
273 {humanSize({ bytes: link.originalSize })}
279 {link.activeRevision?.signatureAddress && (
280 <DetailsRow label={c('Title').t`Last edited by`} dataTestId={'drive:last-edited-by'}>
281 {link.activeRevision?.signatureAddress}
284 <DetailsRow label={c('Title').t`Shared`} dataTestId={'drive:is-shared'}>
285 {link.isShared ? c('Info').t`Yes` : c('Info').t`No`}
287 {link.sharingDetails?.shareUrl && (
289 label={c('Title').t`Public shared link status`}
290 dataTestId={'drive:public-sharing-status'}
292 {link.sharingDetails.shareUrl.isExpired ? c('Info').t`Expired` : c('Info').t`Available`}
296 {(numberOfAccesses !== undefined || isNumberOfAccessesLoading) && (
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()} />
307 {formatAccessCount(numberOfAccesses)}
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">
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}
327 <ModalTwo onClose={onClose} size="large" {...modalProps}>
328 <ModalTwoHeader title={getTitle(link?.isFile)} />
331 <Button onClick={onClose}>{c('Action').t`Close`}</Button>
337 function DetailsRow({ label, title, children, dataTestId }: RowProps) {
340 <span className="label cursor-default">{label}</span>
341 <div className="pt-2" data-testid={dataTestId}>
348 function getTitle(isFile?: boolean) {
349 if (isFile === undefined) {
350 return c('Title').t`Item details`;
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);