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, | |||||
+thumbHashEncryptionKey: ?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, | |||||
}; | |||||
ashoat: Seems like we could move this inside the conditional on line 78, to make it clear it's only… | |||||
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); | ||||
mediaModalItem = ( | mediaModalItem = ( | ||||
<video controls> | <LoadableVideo | ||||
<source src={uri} /> | uri={uri} | ||||
</video> | thumbnailURI={media.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; | ||||
const dimensions = this.state.dimensions ?? media.dimensions; | |||||
const elementStyle = dimensions | |||||
? { | |||||
width: `${dimensions.width}px`, | |||||
height: `${dimensions.height}px`, | |||||
} | |||||
: undefined; | |||||
mediaModalItem = ( | mediaModalItem = ( | ||||
<EncryptedMultimedia | <EncryptedMultimedia | ||||
type={type} | type={type} | ||||
holder={holder} | holder={holder} | ||||
encryptionKey={encryptionKey} | encryptionKey={encryptionKey} | ||||
placeholderSrc={placeholderImage} | |||||
elementStyle={elementStyle} | |||||
/> | /> | ||||
); | ); | ||||
} | } | ||||
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 = () => { | |||||
bartekAuthorUnsubmitted Done Inline ActionsUnfortunately, CSS in encrypted media does not know its width and height before its decrypted so we need this to resize the thumbhash image manually. This function does the same as the following CSS: width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; bartek: Unfortunately, CSS in encrypted media does not know its width and height before its decrypted… | |||||
ashoatUnsubmitted Not Done Inline ActionsThanks for explaining! ashoat: Thanks for explaining! | |||||
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 placeholderImage = usePlaceholder( | |||||
props.media.thumbHash, | |||||
props.media.thumbHashEncryptionKey, | |||||
); | |||||
return <MultimediaModal {...props} popModal={modalContext.popModal} />; | return ( | ||||
<MultimediaModal | |||||
{...props} | |||||
popModal={modalContext.popModal} | |||||
placeholderImage={placeholderImage} | |||||
/> | |||||
); | |||||
} | } | ||||
export default ConnectedMultiMediaModal; | export default ConnectedMultiMediaModal; |
Seems like we could move this inside the conditional on line 78, to make it clear it's only used for that case