{"version":3,"file":"Attachment.cjs","sources":["../../../src/components/internal/Attachment.tsx"],"sourcesContent":["\"use client\";\n\nimport type { CommentMixedAttachment } from \"@liveblocks/core\";\nimport { useRoomAttachmentUrl } from \"@liveblocks/react/_private\";\nimport type {\n  ComponentPropsWithoutRef,\n  KeyboardEvent,\n  MouseEventHandler,\n  PointerEvent,\n} from \"react\";\nimport { memo, useCallback, useMemo, useState } from \"react\";\n\nimport { CrossIcon } from \"../../icons/Cross\";\nimport { SpinnerIcon } from \"../../icons/Spinner\";\nimport { WarningIcon } from \"../../icons/Warning\";\nimport type { Overrides } from \"../../overrides\";\nimport { useOverrides } from \"../../overrides\";\nimport { AttachmentTooLargeError } from \"../../primitives\";\nimport { useComposerAttachmentsContextOrNull } from \"../../primitives/Composer/contexts\";\nimport { classNames } from \"../../utils/class-names\";\nimport { formatFileSize } from \"../../utils/format-file-size\";\nimport { Tooltip } from \"./Tooltip\";\n\nconst MAX_DISPLAYED_MEDIA_SIZE = 60 * 1024 * 1024; // 60 MB\n\ninterface AttachmentProps extends ComponentPropsWithoutRef<\"div\"> {\n  attachment: CommentMixedAttachment;\n  onDeleteClick?: MouseEventHandler<HTMLButtonElement>;\n  preventFocusOnDelete?: boolean;\n  roomId: string;\n  overrides?: Partial<Overrides>;\n  allowMediaPreview?: boolean;\n}\n\nconst fileExtensionRegex = /^(.+?)(\\.[^.]+)?$/;\n\nfunction splitFileName(name: string) {\n  const match = name.match(fileExtensionRegex);\n\n  return { base: match?.[1] ?? name, extension: match?.[2] };\n}\n\nfunction getAttachmentIconGlyph(mimeType: string) {\n  if (\n    mimeType === \"application/zip\" ||\n    mimeType === \"application/gzip\" ||\n    mimeType === \"application/vnd.rar\" ||\n    mimeType === \"application/x-rar-compressed\" ||\n    mimeType === \"application/x-7z-compressed\" ||\n    mimeType === \"application/x-zip-compressed\" ||\n    mimeType === \"application/x-tar\" ||\n    mimeType === \"application/x-bzip\" ||\n    mimeType === \"application/x-bzip2\"\n  ) {\n    return (\n      <path d=\"M13 15h2v1h-1.5a.5.5 0 0 0 0 1H15v1h-1.5a.5.5 0 0 0 0 1H15v1h-1.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 .5-.5V20h1.5a.5.5 0 0 0 0-1H15v-1h1.5a.5.5 0 0 0 0-1H15v-1h1.5a.5.5 0 0 0 .5-.5V15a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2Z\" />\n    );\n  }\n\n  if (\n    mimeType.startsWith(\"text/\") ||\n    mimeType.startsWith(\"font/\") ||\n    mimeType.startsWith(\"application/\")\n  ) {\n    return (\n      <path d=\"M10 16a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5Z\" />\n    );\n  }\n\n  if (mimeType.startsWith(\"image/\")) {\n    return (\n      <path d=\"M12 16h6a1 1 0 0 1 1 1v3l-1.293-1.293a1 1 0 0 0-1.414 0L14.09 20.91l-.464-.386a1 1 0 0 0-1.265-.013l-1.231.985A.995.995 0 0 1 11 21v-4a1 1 0 0 1 1-1Zm-2 1a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-4Zm3 2a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z\" />\n    );\n  }\n\n  if (mimeType.startsWith(\"video/\")) {\n    return (\n      <path d=\"M12 15.71a1 1 0 0 1 1.49-.872l4.96 2.79a1 1 0 0 1 0 1.744l-4.96 2.79A1 1 0 0 1 12 21.29v-5.58Z\" />\n    );\n  }\n\n  if (mimeType.startsWith(\"audio/\")) {\n    return (\n      <path d=\"M15 15a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 1 0v-7a.5.5 0 0 0-.5-.5Zm-2.5 2.5a.5.5 0 0 1 1 0v3a.5.5 0 0 1-1 0v-3Zm-2 1a.5.5 0 0 1 1 0v1a.5.5 0 0 1-1 0v-1Zm6-1a.5.5 0 0 1 1 0v3a.5.5 0 0 1-1 0v-3ZM19 16a.5.5 0 0 0-.5.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5Z\" />\n    );\n  }\n\n  return null;\n}\n\nconst AttachmentFileIcon = memo(({ mimeType }: { mimeType: string }) => {\n  const iconGlyph = useMemo(() => getAttachmentIconGlyph(mimeType), [mimeType]);\n\n  return (\n    <svg\n      className=\"lb-attachment-icon\"\n      width={30}\n      height={30}\n      viewBox=\"0 0 30 30\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M6 5a2 2 0 0 1 2-2h5.843a4 4 0 0 1 2.829 1.172l6.156 6.156A4 4 0 0 1 24 13.157V25a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V5Z\"\n        className=\"lb-attachment-icon-shadow\"\n      />\n      <path\n        d=\"M6 5a2 2 0 0 1 2-2h5.843a4 4 0 0 1 2.829 1.172l6.156 6.156A4 4 0 0 1 24 13.157V25a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V5Z\"\n        className=\"lb-attachment-icon-background\"\n      />\n      <path\n        d=\"M14.382 3.037a4 4 0 0 1 2.29 1.135l6.156 6.157a4 4 0 0 1 1.136 2.289A2 2 0 0 0 22 11h-4a2 2 0 0 1-2-2V5a2 2 0 0 0-1.618-1.963Z\"\n        className=\"lb-attachment-icon-fold\"\n      />\n\n      {iconGlyph && <g className=\"lb-attachment-icon-glyph\">{iconGlyph}</g>}\n    </svg>\n  );\n});\n\nfunction AttachmentImagePreview({\n  attachment,\n  markPreviewAsUnsupported,\n  roomId,\n}: {\n  attachment: CommentMixedAttachment;\n  markPreviewAsUnsupported: () => void;\n  roomId: string;\n}) {\n  const { url } = useRoomAttachmentUrl(attachment.id, roomId);\n  const [isLoaded, setLoaded] = useState(false);\n\n  const handleLoad = useCallback(() => {\n    setLoaded(true);\n  }, []);\n\n  return (\n    <>\n      {!isLoaded ? <SpinnerIcon /> : null}\n      {url ? (\n        <div\n          className=\"lb-attachment-preview-media\"\n          data-hidden={!isLoaded ? \"\" : undefined}\n        >\n          <img\n            src={url}\n            loading=\"lazy\"\n            onLoad={handleLoad}\n            onError={markPreviewAsUnsupported}\n          />\n        </div>\n      ) : null}\n    </>\n  );\n}\n\nfunction AttachmentVideoPreview({\n  attachment,\n  markPreviewAsUnsupported,\n  roomId,\n}: {\n  attachment: CommentMixedAttachment;\n  markPreviewAsUnsupported: () => void;\n  roomId: string;\n}) {\n  const { url } = useRoomAttachmentUrl(attachment.id, roomId);\n  const [isLoaded, setLoaded] = useState(false);\n\n  const handleLoad = useCallback(() => {\n    setLoaded(true);\n  }, []);\n\n  return (\n    <>\n      {!isLoaded ? <SpinnerIcon /> : null}\n      {url ? (\n        <div\n          className=\"lb-attachment-preview-media\"\n          data-hidden={!isLoaded ? \"\" : undefined}\n        >\n          <video\n            src={url}\n            onLoadedData={handleLoad}\n            onError={markPreviewAsUnsupported}\n          />\n        </div>\n      ) : null}\n    </>\n  );\n}\n\nfunction AttachmentPreview({\n  attachment,\n  allowMediaPreview = true,\n  roomId,\n}: {\n  attachment: CommentMixedAttachment;\n  allowMediaPreview?: boolean;\n  roomId: string;\n}) {\n  const [isUnsupportedPreview, setUnsupportedPreview] = useState(false);\n  const isUploaded =\n    attachment.type === \"attachment\" || attachment.status === \"uploaded\";\n\n  function markPreviewAsUnsupported() {\n    setUnsupportedPreview(true);\n  }\n\n  if (\n    !isUnsupportedPreview &&\n    allowMediaPreview &&\n    isUploaded &&\n    attachment.size <= MAX_DISPLAYED_MEDIA_SIZE\n  ) {\n    if (attachment.mimeType.startsWith(\"image/\")) {\n      return (\n        <AttachmentImagePreview\n          attachment={attachment}\n          markPreviewAsUnsupported={markPreviewAsUnsupported}\n          roomId={roomId}\n        />\n      );\n    }\n\n    if (attachment.mimeType.startsWith(\"video/\")) {\n      return (\n        <AttachmentVideoPreview\n          attachment={attachment}\n          markPreviewAsUnsupported={markPreviewAsUnsupported}\n          roomId={roomId}\n        />\n      );\n    }\n  }\n\n  return <AttachmentFileIcon mimeType={attachment.mimeType} />;\n}\n\nfunction AttachmentName({\n  attachment,\n}: {\n  attachment: CommentMixedAttachment;\n}) {\n  const { base: fileBaseName, extension: fileExtension } = useMemo(() => {\n    return splitFileName(attachment.name);\n  }, [attachment.name]);\n\n  return (\n    <span className=\"lb-attachment-name\" title={attachment.name}>\n      <span className=\"lb-attachment-name-base\">{fileBaseName}</span>\n      {fileExtension && (\n        <span className=\"lb-attachment-name-extension\">{fileExtension}</span>\n      )}\n    </span>\n  );\n}\n\nfunction useClickOnKeyDown(\n  onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void\n) {\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event);\n\n      if (event.isDefaultPrevented()) {\n        return;\n      }\n\n      // Simulate a click event on Enter or Space because it's a div\n      if (event.key === \"Enter\" || event.key === \" \") {\n        event.preventDefault();\n\n        const clickEvent = new MouseEvent(\"click\", {\n          bubbles: true,\n          cancelable: true,\n          view: window,\n        });\n        event.target.dispatchEvent(clickEvent);\n      }\n    },\n    [onKeyDown]\n  );\n\n  return handleKeyDown;\n}\n\nfunction useAttachmentContent(\n  attachment: CommentMixedAttachment,\n  overrides?: Partial<Overrides>\n) {\n  const $ = useOverrides(overrides);\n  const composerAttachmentsContext = useComposerAttachmentsContextOrNull();\n  const isInComposer = Boolean(composerAttachmentsContext);\n  const maxAttachmentSize = composerAttachmentsContext?.maxAttachmentSize;\n\n  const status =\n    attachment.type === \"localAttachment\" ? attachment.status : undefined;\n  const isUploading = status === \"uploading\";\n  const isError = status === \"error\";\n\n  let description: string;\n\n  if (attachment.type === \"localAttachment\" && attachment.status === \"error\") {\n    if (attachment.error instanceof AttachmentTooLargeError) {\n      if (attachment.error.origin === \"server\") {\n        description = $.ATTACHMENT_TOO_LARGE();\n      } else {\n        description = $.ATTACHMENT_TOO_LARGE(\n          maxAttachmentSize\n            ? formatFileSize(maxAttachmentSize, $.locale)\n            : undefined\n        );\n      }\n    } else {\n      description = $.ATTACHMENT_ERROR(attachment.error);\n    }\n  } else {\n    description = formatFileSize(attachment.size, $.locale);\n  }\n\n  const deleteLabel = isInComposer\n    ? $.COMPOSER_REMOVE_ATTACHMENT\n    : $.COMMENT_DELETE_ATTACHMENT;\n\n  return {\n    isUploading,\n    isError,\n    description,\n    deleteLabel,\n  };\n}\n\nexport function MediaAttachment({\n  attachment,\n  overrides,\n  onClick,\n  onDeleteClick,\n  preventFocusOnDelete,\n  allowMediaPreview = true,\n  roomId,\n  className,\n  onKeyDown,\n  ...props\n}: AttachmentProps) {\n  const { isUploading, isError, description, deleteLabel } =\n    useAttachmentContent(attachment, overrides);\n\n  const handleDeletePointerDown = useCallback(\n    (event: PointerEvent<HTMLButtonElement>) => {\n      if (preventFocusOnDelete) {\n        event.preventDefault();\n      }\n    },\n    [preventFocusOnDelete]\n  );\n\n  const handleKeyDown = useClickOnKeyDown(onKeyDown);\n\n  return (\n    <div\n      className={classNames(\"lb-attachment lb-media-attachment\", className)}\n      data-error={isError ? \"\" : undefined}\n      {...props}\n      role={onClick ? \"button\" : undefined}\n      onClick={onClick}\n      tabIndex={onClick ? 0 : -1}\n      onKeyDown={onClick ? handleKeyDown : undefined}\n    >\n      <div className=\"lb-attachment-preview\">\n        {isUploading ? (\n          <SpinnerIcon />\n        ) : isError ? (\n          <WarningIcon />\n        ) : (\n          <AttachmentPreview\n            attachment={attachment}\n            allowMediaPreview={allowMediaPreview}\n            roomId={roomId}\n          />\n        )}\n      </div>\n      <div className=\"lb-attachment-details\">\n        <AttachmentName attachment={attachment} />\n        <span className=\"lb-attachment-description\" title={description}>\n          {description}\n        </span>\n      </div>\n      {onDeleteClick && (\n        <Tooltip content={deleteLabel}>\n          <button\n            type=\"button\"\n            className=\"lb-attachment-delete\"\n            onClick={onDeleteClick}\n            onPointerDown={handleDeletePointerDown}\n            aria-label={deleteLabel}\n          >\n            <CrossIcon />\n          </button>\n        </Tooltip>\n      )}\n    </div>\n  );\n}\n\nexport function FileAttachment({\n  attachment,\n  overrides,\n  onClick,\n  onDeleteClick,\n  preventFocusOnDelete,\n  allowMediaPreview = true,\n  roomId,\n  className,\n  onKeyDown,\n  ...props\n}: AttachmentProps) {\n  const { isUploading, isError, description, deleteLabel } =\n    useAttachmentContent(attachment, overrides);\n\n  const handleDeletePointerDown = useCallback(\n    (event: PointerEvent<HTMLButtonElement>) => {\n      if (preventFocusOnDelete) {\n        event.preventDefault();\n      }\n    },\n    [preventFocusOnDelete]\n  );\n\n  const handleKeyDown = useClickOnKeyDown(onKeyDown);\n\n  return (\n    <div\n      className={classNames(\"lb-attachment lb-file-attachment\", className)}\n      data-error={isError ? \"\" : undefined}\n      {...props}\n      role={onClick ? \"button\" : undefined}\n      onClick={onClick}\n      tabIndex={onClick ? 0 : -1}\n      onKeyDown={onClick ? handleKeyDown : undefined}\n    >\n      <div className=\"lb-attachment-preview\">\n        {isUploading ? (\n          <SpinnerIcon />\n        ) : isError ? (\n          <WarningIcon />\n        ) : (\n          <AttachmentPreview\n            attachment={attachment}\n            allowMediaPreview={allowMediaPreview}\n            roomId={roomId}\n          />\n        )}\n      </div>\n      <div className=\"lb-attachment-details\">\n        <AttachmentName attachment={attachment} />\n        <span className=\"lb-attachment-description\" title={description}>\n          {description}\n        </span>\n      </div>\n      {onDeleteClick && (\n        <Tooltip content={deleteLabel}>\n          <button\n            type=\"button\"\n            className=\"lb-attachment-delete\"\n            onClick={onDeleteClick}\n            onPointerDown={handleDeletePointerDown}\n            aria-label={deleteLabel}\n          >\n            <CrossIcon />\n          </button>\n        </Tooltip>\n      )}\n    </div>\n  );\n}\n\nexport function separateMediaAttachments<T extends CommentMixedAttachment>(\n  attachments: T[]\n) {\n  const mediaAttachments: T[] = [];\n  const fileAttachments: T[] = [];\n\n  for (const attachment of attachments) {\n    if (\n      (attachment.mimeType.startsWith(\"image/\") ||\n        attachment.mimeType.startsWith(\"video/\")) &&\n      attachment.size <= MAX_DISPLAYED_MEDIA_SIZE\n    ) {\n      mediaAttachments.push(attachment);\n    } else {\n      fileAttachments.push(attachment);\n    }\n  }\n\n  return {\n    mediaAttachments,\n    fileAttachments,\n  };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAuBA;AAWA;AAEA;AACE;AAEA;AACF;AAEA;AACE;AAWE;AACG;AAAO;AAAoP;AAIhQ;AAKE;AACG;AAAO;AAAiP;AAI7P;AACE;AACG;AAAO;AAAiQ;AAI7Q;AACE;AACG;AAAO;AAAiG;AAI7G;AACE;AACG;AAAO;AAAyP;AAIrQ;AACF;AAEA;AACE;AAEA;AACG;AACW;AACH;AACC;AACA;AACH;AACI;AACA;AACH;AAEN;AAAC;AACG;AACQ;AACZ;AACC;AACG;AACQ;AACZ;AACC;AACG;AACQ;AACZ;AAEe;AAAY;AAA4B;AAAU;AAAA;AAGvE;AAEA;AAAgC;AAC9B;AACA;AAEF;AAKE;AACA;AAEA;AACE;AAAc;AAGhB;AACE;AACG;AAA8B;AAE5B;AACW;AACoB;AAE7B;AACM;AACG;AACA;AACC;AACX;AAEA;AAAA;AAGV;AAEA;AAAgC;AAC9B;AACA;AAEF;AAKE;AACA;AAEA;AACE;AAAc;AAGhB;AACE;AACG;AAA8B;AAE5B;AACW;AACoB;AAE7B;AACM;AACS;AACL;AACX;AAEA;AAAA;AAGV;AAEA;AAA2B;AACzB;AACoB;AAEtB;AAKE;AACA;AAGA;AACE;AAA0B;AAG5B;AAME;AACE;AACG;AACC;AACA;AACA;AACF;AAIJ;AACE;AACG;AACC;AACA;AACA;AACF;AAEJ;AAGF;AAAQ;AAAwC;AAClD;AAEA;AAAwB;AAExB;AAGE;AACE;AAAoC;AAGtC;AACG;AAAe;AAAuC;AACrD;AAAC;AAAe;AAA2B;AAAa;AAErD;AAAe;AAAgC;AAAc;AAAA;AAItE;AAEA;AAGE;AAAsB;AAElB;AAEA;AACE;AAAA;AAIF;AACE;AAEA;AAA2C;AAChC;AACG;AACN;AAER;AAAqC;AACvC;AACF;AACU;AAGZ;AACF;AAEA;AAIE;AACA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AAEA;AACE;AACE;AACE;AAAqC;AAErC;AAAgB;AAGV;AACN;AACF;AAEA;AAAiD;AACnD;AAEA;AAAsD;AAGxD;AAIA;AAAO;AACL;AACA;AACA;AACA;AAEJ;AAEO;AAAyB;AAC9B;AACA;AACA;AACA;AACA;AACoB;AACpB;AACA;AACA;AAEF;AACE;AAGA;AAAgC;AAE5B;AACE;AAAqB;AACvB;AACF;AACqB;AAGvB;AAEA;AACG;AACqE;AACzC;AACvB;AACuB;AAC3B;AACwB;AACa;AAErC;AAAC;AAAc;AAMV;AACC;AACA;AACA;AACF;AAEJ;AACC;AAAc;AACb;AAAC;AAAe;AAAwB;AACvC;AAAe;AAAmC;AAChD;AACH;AAAA;AACF;AAEG;AAAiB;AACf;AACM;AACK;AACD;AACM;AACH;AAED;AACb;AACF;AAAA;AAIR;AAEO;AAAwB;AAC7B;AACA;AACA;AACA;AACA;AACoB;AACpB;AACA;AACA;AAEF;AACE;AAGA;AAAgC;AAE5B;AACE;AAAqB;AACvB;AACF;AACqB;AAGvB;AAEA;AACG;AACoE;AACxC;AACvB;AACuB;AAC3B;AACwB;AACa;AAErC;AAAC;AAAc;AAMV;AACC;AACA;AACA;AACF;AAEJ;AACC;AAAc;AACb;AAAC;AAAe;AAAwB;AACvC;AAAe;AAAmC;AAChD;AACH;AAAA;AACF;AAEG;AAAiB;AACf;AACM;AACK;AACD;AACM;AACH;AAED;AACb;AACF;AAAA;AAIR;AAEO;AAGL;AACA;AAEA;AACE;AAKE;AAAgC;AAEhC;AAA+B;AACjC;AAGF;AAAO;AACL;AACA;AAEJ;;;;"}