Changeset View
Changeset View
Standalone View
Standalone View
web/media/multimedia.react.js
// @flow | // @flow | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { CircularProgressbar } from 'react-circular-progressbar'; | import { CircularProgressbar } from 'react-circular-progressbar'; | ||||
import 'react-circular-progressbar/dist/styles.css'; | import 'react-circular-progressbar/dist/styles.css'; | ||||
import { | import { | ||||
XCircle as XCircleIcon, | XCircle as XCircleIcon, | ||||
AlertCircle as AlertCircleIcon, | AlertCircle as AlertCircleIcon, | ||||
} from 'react-feather'; | } from 'react-feather'; | ||||
import { useModalContext } from 'lib/components/modal-provider.react.js'; | import { useModalContext } from 'lib/components/modal-provider.react.js'; | ||||
import { fetchableMediaURI } from 'lib/media/media-utils.js'; | import { fetchableMediaURI } from 'lib/media/media-utils.js'; | ||||
import type { MediaType, EncryptedMediaType } from 'lib/types/media-types.js'; | import type { | ||||
Dimensions, | |||||
EncryptedMediaType, | |||||
MediaType, | |||||
} from 'lib/types/media-types.js'; | |||||
import EncryptedMultimedia from './encrypted-multimedia.react.js'; | import EncryptedMultimedia from './encrypted-multimedia.react.js'; | ||||
import LoadableVideo from './loadable-video.react.js'; | |||||
import { usePlaceholder } from './media-utils.js'; | |||||
import css from './media.css'; | import css from './media.css'; | ||||
import MultimediaModal from './multimedia-modal.react.js'; | import MultimediaModal from './multimedia-modal.react.js'; | ||||
import Button from '../components/button.react.js'; | import Button from '../components/button.react.js'; | ||||
import { type PendingMultimediaUpload } from '../input/input-state.js'; | import { type PendingMultimediaUpload } from '../input/input-state.js'; | ||||
// this should be in sync with the max-height value | |||||
// for span.multimedia > .multimediaImage in media.css | |||||
const MAX_THUMBNAIL_HEIGHT = 200; | |||||
type MediaSource = | type MediaSource = | ||||
| { | | { | ||||
+type: MediaType, | +type: MediaType, | ||||
+uri: string, | +uri: string, | ||||
+dimensions: ?Dimensions, | |||||
+thumbHash: ?string, | |||||
+thumbnailURI: ?string, | |||||
} | } | ||||
| { | | { | ||||
+type: EncryptedMediaType, | +type: EncryptedMediaType, | ||||
+holder: string, | +holder: string, | ||||
+encryptionKey: string, | +encryptionKey: string, | ||||
+dimensions: ?Dimensions, | |||||
+thumbHash: ?string, | |||||
+thumbnailHolder: ?string, | |||||
+thumbnailEncryptionKey: ?string, | |||||
}; | }; | ||||
type Props = { | type Props = { | ||||
+mediaSource: MediaSource, | +mediaSource: MediaSource, | ||||
+pendingUpload?: ?PendingMultimediaUpload, | +pendingUpload?: ?PendingMultimediaUpload, | ||||
+remove?: (uploadID: string) => void, | +remove?: (uploadID: string) => void, | ||||
+multimediaCSSClass: string, | +multimediaCSSClass: string, | ||||
+multimediaImageCSSClass: string, | +multimediaImageCSSClass: string, | ||||
Show All 36 Lines | (event: SyntheticEvent<HTMLElement>) => { | ||||
); | ); | ||||
removeProp(pendingUpload.localID); | removeProp(pendingUpload.localID); | ||||
}, | }, | ||||
[removeProp, pendingUpload], | [removeProp, pendingUpload], | ||||
); | ); | ||||
const { pushModal } = useModalContext(); | const { pushModal } = useModalContext(); | ||||
const handleClick = React.useCallback(() => { | const handleClick = React.useCallback(() => { | ||||
pushModal(<MultimediaModal media={mediaSource} />); | let media; | ||||
if ( | |||||
mediaSource.type === 'encrypted_photo' || | |||||
mediaSource.type === 'encrypted_video' | |||||
) { | |||||
const { type, holder, encryptionKey } = mediaSource; | |||||
media = { type, holder, encryptionKey }; | |||||
} else { | |||||
const { type, uri } = mediaSource; | |||||
invariant(uri, 'uri is missing for media modal'); | |||||
media = { type, uri }; | |||||
} | |||||
pushModal(<MultimediaModal media={media} />); | |||||
}, [pushModal, mediaSource]); | }, [pushModal, mediaSource]); | ||||
let progressIndicator, errorIndicator, removeButton; | let progressIndicator, errorIndicator, removeButton; | ||||
const { multimediaImageCSSClass, multimediaCSSClass } = props; | const { multimediaImageCSSClass, multimediaCSSClass } = props; | ||||
if (pendingUpload) { | if (pendingUpload) { | ||||
const { progressPercent, failed } = pendingUpload; | const { progressPercent, failed } = pendingUpload; | ||||
Show All 24 Lines | if (removeProp) { | ||||
</Button> | </Button> | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
const imageContainerClasses = [css.multimediaImage, multimediaImageCSSClass]; | const imageContainerClasses = [css.multimediaImage, multimediaImageCSSClass]; | ||||
imageContainerClasses.push(css.clickable); | imageContainerClasses.push(css.clickable); | ||||
const thumbHash = mediaSource.thumbHash ?? pendingUpload?.thumbHash; | |||||
const { encryptionKey, thumbnailEncryptionKey } = mediaSource; | |||||
const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; | |||||
const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); | |||||
const { dimensions } = mediaSource; | |||||
const elementStyle = React.useMemo(() => { | |||||
if (!dimensions) { | |||||
return undefined; | |||||
} | |||||
const { width, height } = dimensions; | |||||
// Resize the image to fit in max width while preserving aspect ratio | |||||
const calculatedWidth = | |||||
Math.min(MAX_THUMBNAIL_HEIGHT, height) * (width / height); | |||||
return { | |||||
background: placeholderImage | |||||
? `center / cover url(${placeholderImage})` | |||||
: undefined, | |||||
width: `${calculatedWidth}px`, | |||||
// height is limited by the max-height style in media.css | |||||
height: `${height}px`, | |||||
}; | |||||
}, [dimensions, placeholderImage]); | |||||
// Media element is the actual image or video element (or encrypted version) | // Media element is the actual image or video element (or encrypted version) | ||||
let mediaElement; | let mediaElement; | ||||
if (mediaSource.type === 'photo') { | if (mediaSource.type === 'photo') { | ||||
const uri = fetchableMediaURI(mediaSource.uri); | const uri = fetchableMediaURI(mediaSource.uri); | ||||
mediaElement = <img src={uri} />; | mediaElement = <img src={uri} style={elementStyle} />; | ||||
} else if (mediaSource.type === 'video') { | } else if (mediaSource.type === 'video') { | ||||
const uri = fetchableMediaURI(mediaSource.uri); | const uri = fetchableMediaURI(mediaSource.uri); | ||||
const { thumbnailURI } = mediaSource; | |||||
invariant(thumbnailURI, 'video missing thumbnail'); | |||||
mediaElement = ( | mediaElement = ( | ||||
<video controls> | <LoadableVideo | ||||
<source src={uri} /> | uri={uri} | ||||
</video> | thumbnailSource={{ thumbnailURI }} | ||||
thumbHashDataURL={placeholderImage} | |||||
elementStyle={elementStyle} | |||||
/> | |||||
); | ); | ||||
} else if ( | } else if ( | ||||
mediaSource.type === 'encrypted_photo' || | mediaSource.type === 'encrypted_photo' || | ||||
mediaSource.type === 'encrypted_video' | mediaSource.type === 'encrypted_video' | ||||
) { | ) { | ||||
mediaElement = <EncryptedMultimedia {...mediaSource} />; | const { type, holder } = mediaSource; | ||||
invariant(encryptionKey, 'encryptionKey undefined for encrypted media'); | |||||
mediaElement = ( | |||||
<EncryptedMultimedia | |||||
type={type} | |||||
holder={holder} | |||||
encryptionKey={encryptionKey} | |||||
/> | |||||
); | |||||
} | } | ||||
// Media node is the container for the media element (button if photo) | // Media node is the container for the media element (button if photo) | ||||
let mediaNode; | let mediaNode; | ||||
if (mediaSource.type === 'photo' || mediaSource.type === 'encrypted_photo') { | if (mediaSource.type === 'photo' || mediaSource.type === 'encrypted_photo') { | ||||
mediaNode = ( | mediaNode = ( | ||||
<Button | <Button | ||||
className={classNames(imageContainerClasses)} | className={classNames(imageContainerClasses)} | ||||
Show All 26 Lines |