RichAttachment.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import React, { useCallback } from 'react';
  2. import { UserPicture } from '@growi/ui/dist/components';
  3. import { useTranslation } from 'next-i18next';
  4. import Image from 'next/image';
  5. import prettyBytes from 'pretty-bytes';
  6. import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores-universal/context';
  7. import { useSWRxAttachment } from '~/stores/attachment';
  8. import { useDeleteAttachmentModal } from '~/stores/modal';
  9. import styles from './RichAttachment.module.scss';
  10. type RichAttachmentProps = {
  11. attachmentId: string,
  12. url: string,
  13. attachmentName: string,
  14. }
  15. export const RichAttachment = React.memo((props: RichAttachmentProps) => {
  16. const { attachmentId, attachmentName } = props;
  17. const { t } = useTranslation();
  18. const { data: attachment, remove } = useSWRxAttachment(attachmentId);
  19. const { open: openDeleteAttachmentModal } = useDeleteAttachmentModal();
  20. const { data: isGuestUser } = useIsGuestUser();
  21. const { data: isSharedUser } = useIsSharedUser();
  22. const { data: isReadOnlyUser } = useIsReadOnlyUser();
  23. const showTrashButton = isGuestUser === false && isSharedUser === false && isReadOnlyUser === false;
  24. const onClickTrashButtonHandler = useCallback(() => {
  25. if (attachment == null) {
  26. return;
  27. }
  28. openDeleteAttachmentModal(attachment, remove);
  29. }, [attachment, openDeleteAttachmentModal, remove]);
  30. if (attachment == null) {
  31. return <span className="text-muted">{t('rich_attachment.attachment_not_be_found')}</span>;
  32. }
  33. const {
  34. filePathProxied,
  35. originalName,
  36. downloadPathProxied,
  37. creator,
  38. createdAt,
  39. fileSize,
  40. } = attachment;
  41. // Guard here because attachment properties might be deleted in turn when an attachment is removed
  42. if (filePathProxied == null
  43. || originalName == null
  44. || downloadPathProxied == null
  45. || creator == null
  46. || createdAt == null
  47. || fileSize == null
  48. ) {
  49. return <span className="text-muted">{t('rich_attachment.attachment_not_be_found')}</span>;
  50. }
  51. return (
  52. <div data-testid="rich-attachment" className={`${styles.attachment} d-inline-block`}>
  53. <div className="my-2 p-2 card">
  54. <div className="p-1 card-body d-flex align-items-center">
  55. <div className="me-2 px-0 d-flex align-items-center justify-content-center">
  56. <Image
  57. width={20}
  58. height={20}
  59. src="/images/icons/editor/attachment.svg"
  60. className="attachment-icon"
  61. alt="attachment icon"
  62. />
  63. </div>
  64. <div className="ps-0">
  65. <div className="d-inline-block">
  66. {/* Since we need to include the "referer" to view the attachment on the shared page */}
  67. {/* eslint-disable-next-line react/jsx-no-target-blank */}
  68. <a target="_blank" rel="noopener" href={filePathProxied}>
  69. {attachmentName || originalName}
  70. </a>
  71. <a className="ms-2 attachment-download" href={downloadPathProxied}>
  72. <span className="material-symbols-outlined">cloud_download</span>
  73. </a>
  74. {showTrashButton && (
  75. <a className="ml-2 text-danger attachment-delete d-share-link-none" type="button" onClick={onClickTrashButtonHandler}>
  76. <span className="material-symbols-outlined">delete</span>
  77. </a>
  78. )}
  79. </div>
  80. <div className="d-flex align-items-center">
  81. <UserPicture user={creator} size="sm" />
  82. <span className="ms-2 text-muted">
  83. {new Date(createdAt).toLocaleString('en-US')}
  84. </span>
  85. <span className="ms-2 ps-2 border-start text-muted">{prettyBytes(fileSize)}</span>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. );
  92. });
  93. RichAttachment.displayName = 'RichAttachment';