Files
2025-10-12 13:42:02 +02:00

156 lines
4.5 KiB
React

import PropTypes from "prop-types";
import classNames from "classnames";
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { Blurhash } from "mastodon/components/blurhash";
import { Icon } from "mastodon/components/icon";
import { autoPlayGif, displayMedia, useBlurhash } from "mastodon/initial_state";
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
displayWidth: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
};
state = {
visible: displayMedia !== "hide_all" && !this.props.attachment.getIn(["status", "sensitive"]) || 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 () {
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 } = 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, label, icon, content;
if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='eye-slash' />
</span>
);
} else {
if (["audio", "video"].includes(attachment.get("type"))) {
content = (
<img
src={attachment.get("preview_url") || status.getIn(["account", "avatar_static"])}
alt={attachment.get("description")}
lang={status.get("language")}
onLoad={this.handleImageLoad}
/>
);
if (attachment.get("type") === "audio") {
label = <Icon id='music' />;
} else {
label = <Icon id='play' />;
}
} 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;
content = (
<img
src={attachment.get("preview_url")}
alt={attachment.get("description")}
lang={status.get("language")}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (attachment.get("type") === "gifv") {
content = (
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get("description")}
title={attachment.get("description")}
lang={status.get("language")}
role='application'
src={attachment.get("url")}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
);
label = "GIF";
}
thumbnail = (
<div className='media-gallery__gifv'>
{content}
{label && (
<div className='media-gallery__item__badges'>
<span className='media-gallery__gifv__label'>{label}</span>
</div>
)}
</div>
);
}
return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={`/@${status.getIn(["account", "acct"])}/${status.get("id")}`} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get("blurhash")}
className={classNames("media-gallery__preview", { "media-gallery__preview--hidden": visible && loaded })}
dummy={!useBlurhash}
/>
{visible ? thumbnail : icon}
</a>
</div>
);
}
}