Files
cinny/src/app/molecules/media/Media.jsx
T

372 lines
9.4 KiB
React
Raw Normal View History

2021-07-28 18:45:52 +05:30
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './Media.scss';
import encrypt from 'browser-encrypt-attachment';
2022-08-06 05:56:26 +02:00
import { BlurhashCanvas } from 'react-blurhash';
2021-07-28 18:45:52 +05:30
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Spinner from '../../atoms/spinner/Spinner';
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
2022-07-18 17:33:11 +01:00
// https://github.com/matrix-org/matrix-react-sdk/blob/cd15e08fc285da42134817cce50de8011809cd53/src/utils/blobs.ts#L73
2021-07-28 18:45:52 +05:30
const ALLOWED_BLOB_MIMETYPES = [
'image/jpeg',
'image/gif',
'image/png',
2022-07-18 17:33:11 +01:00
'image/apng',
'image/webp',
'image/avif',
2021-07-28 18:45:52 +05:30
'video/mp4',
'video/webm',
'video/ogg',
2022-07-18 17:33:11 +01:00
'video/quicktime',
2021-07-28 18:45:52 +05:30
'audio/mp4',
'audio/webm',
'audio/aac',
'audio/mpeg',
'audio/ogg',
'audio/wave',
'audio/wav',
'audio/x-wav',
'audio/x-pn-wav',
'audio/flac',
'audio/x-flac',
];
function getBlobSafeMimeType(mimetype) {
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
return 'application/octet-stream';
}
2022-07-18 17:33:11 +01:00
// Required for Chromium browsers
if (mimetype === 'video/quicktime') {
return 'video/mp4';
}
2021-07-28 18:45:52 +05:30
return mimetype;
}
async function getDecryptedBlob(response, type, decryptData) {
const arrayBuffer = await response.arrayBuffer();
const dataArray = await encrypt.decryptAttachment(arrayBuffer, decryptData);
const blob = new Blob([dataArray], { type: getBlobSafeMimeType(type) });
return blob;
}
async function getUrl(link, type, decryptData) {
try {
const response = await fetch(link, { method: 'GET' });
if (decryptData !== null) {
return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData));
}
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (e) {
return link;
}
}
2022-08-06 09:04:23 +05:30
function getNativeHeight(width, height, maxWidth = 296) {
const scale = maxWidth / width;
2021-07-28 18:45:52 +05:30
return scale * height;
}
function FileHeader({
name, link, external,
file, type,
}) {
const [url, setUrl] = useState(null);
async function getFile() {
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
}
async function handleDownload(e) {
if (file !== null && url === null) {
e.preventDefault();
await getFile();
e.target.click();
}
}
return (
<div className="file-header">
<Text className="file-name" variant="b3">{name}</Text>
{ link !== null && (
<>
{
external && (
<IconButton
size="extra-small"
tooltip="Open in new tab"
src={ExternalSVG}
onClick={() => window.open(url || link)}
/>
)
}
<a href={url || link} download={name} target="_blank" rel="noreferrer">
<IconButton
size="extra-small"
tooltip="Download"
src={DownloadSVG}
onClick={handleDownload}
/>
</a>
</>
)}
</div>
);
}
FileHeader.defaultProps = {
external: false,
file: null,
link: null,
};
FileHeader.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string,
external: PropTypes.bool,
file: PropTypes.shape({}),
type: PropTypes.string.isRequired,
};
function File({
name, link, file, type,
}) {
return (
<div className="file-container">
<FileHeader name={name} link={link} file={file} type={type} />
</div>
);
}
File.defaultProps = {
file: null,
2021-08-18 13:55:44 +05:30
type: '',
2021-07-28 18:45:52 +05:30
};
File.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
2021-08-18 13:55:44 +05:30
type: PropTypes.string,
2021-07-28 18:45:52 +05:30
file: PropTypes.shape({}),
};
function Image({
2022-08-06 05:56:26 +02:00
name, width, height, link, file, type, blurhash,
2021-07-28 18:45:52 +05:30
}) {
const [url, setUrl] = useState(null);
const [blur, setBlur] = useState(true);
2021-07-28 18:45:52 +05:30
useEffect(() => {
let unmounted = false;
async function fetchUrl() {
const myUrl = await getUrl(link, type, file);
if (unmounted) return;
setUrl(myUrl);
}
fetchUrl();
return () => {
unmounted = true;
};
}, []);
return (
<div className="file-container">
<FileHeader name={name} link={url || link} type={type} external />
<div style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }} className="image-container">
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
{ url !== null && <img style={{ display: blur ? 'none' : 'unset' }} onLoad={() => setBlur(false)} src={url || link} alt={name} />}
2021-07-28 18:45:52 +05:30
</div>
</div>
);
}
Image.defaultProps = {
file: null,
width: null,
height: null,
2021-08-18 13:55:44 +05:30
type: '',
2022-08-06 05:56:26 +02:00
blurhash: '',
2021-07-28 18:45:52 +05:30
};
Image.propTypes = {
name: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
link: PropTypes.string.isRequired,
file: PropTypes.shape({}),
2021-08-18 13:55:44 +05:30
type: PropTypes.string,
2022-08-06 05:56:26 +02:00
blurhash: PropTypes.string,
2021-07-28 18:45:52 +05:30
};
2022-08-06 09:04:23 +05:30
function Sticker({
name, height, width, link, file, type,
}) {
const [url, setUrl] = useState(null);
useEffect(() => {
let unmounted = false;
async function fetchUrl() {
const myUrl = await getUrl(link, type, file);
if (unmounted) return;
setUrl(myUrl);
}
fetchUrl();
return () => {
unmounted = true;
};
}, []);
return (
<div className="sticker-container" style={{ height: width !== null ? getNativeHeight(width, height, 128) : 'unset' }}>
{ url !== null && <img src={url || link} title={name} alt={name} />}
</div>
);
}
Sticker.defaultProps = {
file: null,
type: '',
width: null,
height: null,
2022-08-06 09:04:23 +05:30
};
Sticker.propTypes = {
name: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
link: PropTypes.string.isRequired,
file: PropTypes.shape({}),
type: PropTypes.string,
};
2021-07-28 18:45:52 +05:30
function Audio({
name, link, type, file,
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
async function loadAudio() {
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
setIsLoading(false);
}
function handlePlayAudio() {
setIsLoading(true);
loadAudio();
}
return (
<div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div className="audio-container">
{ url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
{ url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
<audio autoPlay controls>
<source src={url} type={getBlobSafeMimeType(type)} />
</audio>
)}
</div>
</div>
);
}
Audio.defaultProps = {
file: null,
2021-08-18 13:55:44 +05:30
type: '',
2021-07-28 18:45:52 +05:30
};
Audio.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
2021-08-18 13:55:44 +05:30
type: PropTypes.string,
2021-07-28 18:45:52 +05:30
file: PropTypes.shape({}),
};
function Video({
2022-08-06 05:56:26 +02:00
name, link, thumbnail, thumbnailFile, thumbnailType,
width, height, file, type, blurhash,
2021-07-28 18:45:52 +05:30
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
const [thumbUrl, setThumbUrl] = useState(null);
const [blur, setBlur] = useState(true);
2021-07-28 18:45:52 +05:30
useEffect(() => {
let unmounted = false;
async function fetchUrl() {
const myThumbUrl = await getUrl(thumbnail, thumbnailType, thumbnailFile);
if (unmounted) return;
setThumbUrl(myThumbUrl);
}
if (thumbnail !== null) fetchUrl();
return () => {
unmounted = true;
};
}, []);
const loadVideo = async () => {
2021-07-28 18:45:52 +05:30
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
setIsLoading(false);
};
2021-07-28 18:45:52 +05:30
const handlePlayVideo = () => {
2021-07-28 18:45:52 +05:30
setIsLoading(true);
loadVideo();
};
2021-07-28 18:45:52 +05:30
return (
<div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div
style={{
height: width !== null ? getNativeHeight(width, height) : 'unset',
}}
className="video-container"
>
{ url === null ? (
<>
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
{ thumbUrl !== null && (
<img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
)}
{isLoading && <Spinner size="small" />}
{!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
</>
) : (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
2021-07-28 18:45:52 +05:30
<video autoPlay controls poster={thumbUrl}>
<source src={url} type={getBlobSafeMimeType(type)} />
</video>
)}
</div>
</div>
);
}
Video.defaultProps = {
width: null,
height: null,
file: null,
thumbnail: null,
thumbnailType: null,
thumbnailFile: null,
2022-08-06 05:56:26 +02:00
type: '',
blurhash: null,
2021-07-28 18:45:52 +05:30
};
Video.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
thumbnail: PropTypes.string,
2022-08-06 05:56:26 +02:00
thumbnailFile: PropTypes.shape({}),
thumbnailType: PropTypes.string,
2021-07-28 18:45:52 +05:30
width: PropTypes.number,
height: PropTypes.number,
file: PropTypes.shape({}),
2021-08-18 13:55:44 +05:30
type: PropTypes.string,
2022-08-06 05:56:26 +02:00
blurhash: PropTypes.string,
2021-07-28 18:45:52 +05:30
};
export {
2022-08-06 09:04:23 +05:30
File, Image, Sticker, Audio, Video,
2021-07-28 18:45:52 +05:30
};