Changeset View
Changeset View
Standalone View
Standalone View
web/media/multimedia-modal.react.js
// @flow | // @flow | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { XCircle as XCircleIcon } from 'react-feather'; | import { XCircle as XCircleIcon } 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 { EncryptedMediaType, MediaType } from 'lib/types/media-types.js'; | import type { | ||||
EncryptedMediaType, | |||||
MediaType, | |||||
Dimensions, | |||||
} 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'; | ||||
type MediaInfo = | type MediaInfo = | ||||
| { | | { | ||||
+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 BaseProps = { | type BaseProps = { | ||||
+media: MediaInfo, | +media: MediaInfo, | ||||
}; | }; | ||||
type Props = { | type Props = { | ||||
...BaseProps, | ...BaseProps, | ||||
+popModal: (modal: ?React.Node) => void, | +popModal: (modal: ?React.Node) => void, | ||||
+placeholderImage: ?string, | |||||
}; | }; | ||||
class MultimediaModal extends React.PureComponent<Props> { | type State = { | ||||
+dimensions: ?Dimensions, | |||||
}; | |||||
class MultimediaModal extends React.PureComponent<Props, State> { | |||||
overlay: ?HTMLDivElement; | overlay: ?HTMLDivElement; | ||||
constructor(props: Props) { | |||||
super(props); | |||||
this.state = { dimensions: null }; | |||||
} | |||||
componentDidMount() { | componentDidMount() { | ||||
invariant(this.overlay, 'overlay ref unset'); | invariant(this.overlay, 'overlay ref unset'); | ||||
this.overlay.focus(); | this.overlay.focus(); | ||||
this.calculateMediaDimensions(); | |||||
window.addEventListener('resize', this.calculateMediaDimensions); | |||||
} | |||||
componentWillUnmount() { | |||||
window.removeEventListener('resize', this.calculateMediaDimensions); | |||||
} | } | ||||
render(): React.Node { | render(): React.Node { | ||||
let mediaModalItem; | let mediaModalItem; | ||||
const { media } = this.props; | const { media, placeholderImage } = this.props; | ||||
const style = { | |||||
backgroundImage: placeholderImage | |||||
? `url(${placeholderImage})` | |||||
: undefined, | |||||
}; | |||||
if (media.type === 'photo') { | if (media.type === 'photo') { | ||||
const uri = fetchableMediaURI(media.uri); | const uri = fetchableMediaURI(media.uri); | ||||
mediaModalItem = <img src={uri} />; | mediaModalItem = <img src={uri} style={style} />; | ||||
} else if (media.type === 'video') { | } else if (media.type === 'video') { | ||||
const uri = fetchableMediaURI(media.uri); | const uri = fetchableMediaURI(media.uri); | ||||
const { thumbnailURI } = media; | |||||
invariant(thumbnailURI, 'video missing thumbnail'); | |||||
mediaModalItem = ( | mediaModalItem = ( | ||||
<video controls> | <LoadableVideo | ||||
<source src={uri} /> | uri={uri} | ||||
</video> | thumbnailSource={{ thumbnailURI }} | ||||
thumbHashDataURL={placeholderImage} | |||||
/> | |||||
); | ); | ||||
} else { | } else { | ||||
invariant( | invariant( | ||||
media.type === 'encrypted_photo' || media.type === 'encrypted_video', | media.type === 'encrypted_photo' || media.type === 'encrypted_video', | ||||
'invalid media type', | 'invalid media type', | ||||
); | ); | ||||
const { type, holder, encryptionKey } = media; | const { type, holder, encryptionKey } = media; | ||||
mediaModalItem = ( | mediaModalItem = ( | ||||
<EncryptedMultimedia | <EncryptedMultimedia | ||||
type={type} | type={type} | ||||
holder={holder} | holder={holder} | ||||
encryptionKey={encryptionKey} | encryptionKey={encryptionKey} | ||||
/> | /> | ||||
); | ); | ||||
} | } | ||||
return ( | return ( | ||||
<div | <div | ||||
className={css.multimediaModalOverlay} | className={css.multimediaModalOverlay} | ||||
ref={this.overlayRef} | ref={this.overlayRef} | ||||
onClick={this.onBackgroundClick} | onClick={this.onBackgroundClick} | ||||
tabIndex={0} | tabIndex={0} | ||||
onKeyDown={this.onKeyDown} | onKeyDown={this.onKeyDown} | ||||
> | > | ||||
{mediaModalItem} | <div className={css.mediaContainer}>{mediaModalItem}</div> | ||||
<XCircleIcon | <XCircleIcon | ||||
onClick={this.props.popModal} | onClick={this.props.popModal} | ||||
className={css.closeMultimediaModal} | className={css.closeMultimediaModal} | ||||
/> | /> | ||||
</div> | </div> | ||||
); | ); | ||||
} | } | ||||
Show All 9 Lines | onBackgroundClick: (event: SyntheticEvent<HTMLDivElement>) => void = | ||||
}; | }; | ||||
onKeyDown: (event: SyntheticKeyboardEvent<HTMLDivElement>) => void = | onKeyDown: (event: SyntheticKeyboardEvent<HTMLDivElement>) => void = | ||||
event => { | event => { | ||||
if (event.key === 'Escape') { | if (event.key === 'Escape') { | ||||
this.props.popModal(); | this.props.popModal(); | ||||
} | } | ||||
}; | }; | ||||
calculateMediaDimensions: () => void = () => { | |||||
if (!this.overlay || !this.props.media.dimensions) { | |||||
return; | |||||
} | |||||
const containerWidth = this.overlay.clientWidth; | |||||
const containerHeight = this.overlay.clientHeight; | |||||
const containerAspectRatio = containerWidth / containerHeight; | |||||
const { width: mediaWidth, height: mediaHeight } = | |||||
this.props.media.dimensions; | |||||
const mediaAspectRatio = mediaWidth / mediaHeight; | |||||
let newWidth, newHeight; | |||||
if (containerAspectRatio > mediaAspectRatio) { | |||||
newWidth = Math.min(mediaWidth, containerHeight * mediaAspectRatio); | |||||
newHeight = newWidth / mediaAspectRatio; | |||||
} else { | |||||
newHeight = Math.min(mediaHeight, containerWidth / mediaAspectRatio); | |||||
newWidth = newHeight * mediaAspectRatio; | |||||
} | |||||
this.setState({ | |||||
dimensions: { | |||||
width: newWidth, | |||||
height: newHeight, | |||||
}, | |||||
}); | |||||
}; | |||||
} | } | ||||
function ConnectedMultiMediaModal(props: BaseProps): React.Node { | function ConnectedMultiMediaModal(props: BaseProps): React.Node { | ||||
const modalContext = useModalContext(); | const modalContext = useModalContext(); | ||||
const { thumbHash, encryptionKey, thumbnailEncryptionKey } = props.media; | |||||
const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; | |||||
const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); | |||||
return <MultimediaModal {...props} popModal={modalContext.popModal} />; | return ( | ||||
<MultimediaModal | |||||
{...props} | |||||
popModal={modalContext.popModal} | |||||
placeholderImage={placeholderImage} | |||||
/> | |||||
); | |||||
} | } | ||||
export default ConnectedMultiMediaModal; | export default ConnectedMultiMediaModal; |