diff --git a/app/soapbox/features/account_gallery/components/media_item.js b/app/soapbox/features/account_gallery/components/media_item.js
deleted file mode 100644
index fe8c6cc85..000000000
--- a/app/soapbox/features/account_gallery/components/media_item.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { getSettings } from 'soapbox/actions/settings';
-import Blurhash from 'soapbox/components/blurhash';
-import Icon from 'soapbox/components/icon';
-import StillImage from 'soapbox/components/still_image';
-import { isIOS } from 'soapbox/is_mobile';
-
-const mapStateToProps = state => ({
- autoPlayGif: getSettings(state).get('autoPlayGif'),
- displayMedia: getSettings(state).get('displayMedia'),
-});
-
-export default @connect(mapStateToProps)
-class MediaItem extends ImmutablePureComponent {
-
- static propTypes = {
- attachment: ImmutablePropTypes.map.isRequired,
- displayWidth: PropTypes.number.isRequired,
- onOpenMedia: PropTypes.func.isRequired,
- autoPlayGif: PropTypes.bool,
- displayMedia: PropTypes.string,
- };
-
- state = {
- visible: this.props.displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || this.props.displayMedia === 'show_all',
- loaded: false,
- };
-
- handleImageLoad = () => {
- this.setState({ loaded: true });
- }
-
- handleMouseEnter = e => {
- if (this.hoverToPlay()) {
- e.target.play();
- }
- }
-
- handleMouseLeave = e => {
- if (this.hoverToPlay()) {
- e.target.pause();
- e.target.currentTime = 0;
- }
- }
-
- hoverToPlay = () => {
- const { autoPlayGif } = this.props;
- return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
- }
-
- handleClick = e => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
-
- if (this.state.visible) {
- this.props.onOpenMedia(this.props.attachment);
- } else {
- this.setState({ visible: true });
- }
- }
- }
-
- render() {
- const { attachment, displayWidth, autoPlayGif } = this.props;
- const { visible, loaded } = this.state;
-
- const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
- const height = width;
- const status = attachment.get('status');
- const title = status.get('spoiler_text') || attachment.get('description');
-
- let thumbnail = '';
- let icon;
-
- if (attachment.get('type') === 'unknown') {
- // Skip
- } else if (attachment.get('type') === 'image') {
- const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
- const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
- const x = ((focusX / 2) + .5) * 100;
- const y = ((focusY / -2) + .5) * 100;
-
- thumbnail = (
-
- );
- } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
- const conditionalAttributes = {};
- if (isIOS()) {
- conditionalAttributes.playsInline = '1';
- }
- if (autoPlayGif) {
- conditionalAttributes.autoPlay = '1';
- }
- thumbnail = (
-
-
-
- GIF
-
- );
- } else if (attachment.get('type') === 'audio') {
- const remoteURL = attachment.get('remote_url') || '';
- const fileExtensionLastIndex = remoteURL.lastIndexOf('.');
- const fileExtension = remoteURL.substr(fileExtensionLastIndex + 1).toUpperCase();
- thumbnail = (
-
-
- {fileExtension}
-
- );
- }
-
- if (!visible) {
- icon = (
-
-
-
- );
- }
-
- return (
-
- );
- }
-
-}
diff --git a/app/soapbox/features/account_gallery/components/media_item.tsx b/app/soapbox/features/account_gallery/components/media_item.tsx
new file mode 100644
index 000000000..c113ac5e6
--- /dev/null
+++ b/app/soapbox/features/account_gallery/components/media_item.tsx
@@ -0,0 +1,141 @@
+import classNames from 'classnames';
+import React, { useState } from 'react';
+
+import Blurhash from 'soapbox/components/blurhash';
+import Icon from 'soapbox/components/icon';
+import StillImage from 'soapbox/components/still_image';
+import { useSettings } from 'soapbox/hooks';
+import { isIOS } from 'soapbox/is_mobile';
+
+import type { Attachment } from 'soapbox/types/entities';
+
+interface IMediaItem {
+ attachment: Attachment,
+ displayWidth: number,
+ onOpenMedia: (attachment: Attachment) => void,
+}
+
+const MediaItem: React.FC = ({ attachment, displayWidth, onOpenMedia }) => {
+ const settings = useSettings();
+ const autoPlayGif = settings.get('autoPlayGif');
+ const displayMedia = settings.get('displayMedia');
+
+ const [visible, setVisible] = useState(displayMedia !== 'hide_all' && !attachment.status?.sensitive || displayMedia === 'show_all');
+
+ const handleMouseEnter: React.MouseEventHandler = e => {
+ const video = e.target as HTMLVideoElement;
+ if (hoverToPlay()) {
+ video.play();
+ }
+ };
+
+ const handleMouseLeave: React.MouseEventHandler = e => {
+ const video = e.target as HTMLVideoElement;
+ if (hoverToPlay()) {
+ video.pause();
+ video.currentTime = 0;
+ }
+ };
+
+ const hoverToPlay = () => {
+ return !autoPlayGif && ['gifv', 'video'].indexOf(attachment.type) !== -1;
+ };
+
+ const handleClick: React.MouseEventHandler = e => {
+ if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+
+ if (visible) {
+ onOpenMedia(attachment);
+ } else {
+ setVisible(true);
+ }
+ }
+ };
+
+ const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
+ const height = width;
+ const status = attachment.get('status');
+ const title = status.get('spoiler_text') || attachment.get('description');
+
+ let thumbnail: React.ReactNode = '';
+ let icon;
+
+ if (attachment.type === 'unknown') {
+ // Skip
+ } else if (attachment.type === 'image') {
+ const focusX = Number(attachment.getIn(['meta', 'focus', 'x'])) || 0;
+ const focusY = Number(attachment.getIn(['meta', 'focus', 'y'])) || 0;
+ const x = ((focusX / 2) + .5) * 100;
+ const y = ((focusY / -2) + .5) * 100;
+
+ thumbnail = (
+
+ );
+ } else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {
+ const conditionalAttributes: React.VideoHTMLAttributes = {};
+ if (isIOS()) {
+ conditionalAttributes.playsInline = true;
+ }
+ if (autoPlayGif) {
+ conditionalAttributes.autoPlay = true;
+ }
+ thumbnail = (
+
+
+
+ GIF
+
+ );
+ } else if (attachment.type === 'audio') {
+ const remoteURL = attachment.remote_url || '';
+ const fileExtensionLastIndex = remoteURL.lastIndexOf('.');
+ const fileExtension = remoteURL.substr(fileExtensionLastIndex + 1).toUpperCase();
+ thumbnail = (
+
+
+ {fileExtension}
+
+ );
+ }
+
+ if (!visible) {
+ icon = (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default MediaItem;