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, | |||||||||||||||
}; | |||||||||||||||
bartek: This state is used in the next diff: D7900 | |||||||||||||||
class MultimediaModal extends React.PureComponent<Props, State> { | |||||||||||||||
overlay: ?HTMLDivElement; | overlay: ?HTMLDivElement; | ||||||||||||||
constructor(props: Props) { | |||||||||||||||
super(props); | |||||||||||||||
this.state = { dimensions: null }; | |||||||||||||||
} | |||||||||||||||
ashoatUnsubmitted Not Done Inline Actions
Arguably this way is cleaner, but both work ashoat: Arguably this way is cleaner, but both work | |||||||||||||||
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; |
This state is used in the next diff: D7900