import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { changeUploadCompose, uploadThumbnail } from '../../../actions/compose';
import { getPointerPosition } from '../../video';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button';
import Video from 'mastodon/features/video';
import Audio from 'mastodon/features/audio';
import Textarea from 'react-textarea-autosize';
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import GIFV from 'mastodon/components/gifv';
import { me } from 'mastodon/initial_state';
// eslint-disable-next-line import/no-extraneous-dependencies
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
// eslint-disable-next-line import/extensions
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
import { assetHost } from 'mastodon/utils/config';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
});
const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
account: state.getIn(['accounts', me]),
isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
});
const mapDispatchToProps = (dispatch, { id }) => ({
onSave: (description, x, y) => {
dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
},
onSelectThumbnail: files => {
dispatch(uploadThumbnail(id, files[0]));
},
});
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
.replace(/\n/g, ' ')
.replace(/\*\*\*\*\*\*/g, '\n\n');
class ImageLoader extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
};
state = {
loading: true,
};
componentDidMount() {
const image = new Image();
image.addEventListener('load', () => this.setState({ loading: false }));
image.src = this.props.src;
}
render () {
const { loading } = this.state;
if (loading) {
return ;
} else {
return ;
}
}
}
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class FocalPointModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
account: ImmutablePropTypes.map.isRequired,
isUploadingThumbnail: PropTypes.bool,
onSave: PropTypes.func.isRequired,
onSelectThumbnail: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
x: 0,
y: 0,
focusX: 0,
focusY: 0,
dragging: false,
description: '',
dirty: false,
progress: 0,
loading: true,
ocrStatus: '',
};
componentWillMount () {
this.updatePositionFromMedia(this.props.media);
}
componentWillReceiveProps (nextProps) {
if (this.props.media.get('id') !== nextProps.media.get('id')) {
this.updatePositionFromMedia(nextProps.media);
}
}
componentWillUnmount () {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
this.updatePosition(e);
this.setState({ dragging: true });
}
handleTouchStart = e => {
document.addEventListener('touchmove', this.handleMouseMove);
document.addEventListener('touchend', this.handleTouchEnd);
this.updatePosition(e);
this.setState({ dragging: true });
}
handleMouseMove = e => {
this.updatePosition(e);
}
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
this.setState({ dragging: false });
}
handleTouchEnd = () => {
document.removeEventListener('touchmove', this.handleMouseMove);
document.removeEventListener('touchend', this.handleTouchEnd);
this.setState({ dragging: false });
}
updatePosition = e => {
const { x, y } = getPointerPosition(this.node, e);
const focusX = (x - .5) * 2;
const focusY = (y - .5) * -2;
this.setState({ x, y, focusX, focusY, dirty: true });
}
updatePositionFromMedia = media => {
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
const description = media.get('description') || '';
if (focusX && focusY) {
const x = (focusX / 2) + .5;
const y = (focusY / -2) + .5;
this.setState({
x,
y,
focusX,
focusY,
description,
dirty: false,
});
} else {
this.setState({
x: 0.5,
y: 0.5,
focusX: 0,
focusY: 0,
description,
dirty: false,
});
}
}
handleChange = e => {
this.setState({ description: e.target.value, dirty: true });
}
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
this.setState({ description: e.target.value, dirty: true });
this.handleSubmit();
}
}
handleSubmit = () => {
this.props.onSave(this.state.description, this.state.focusX, this.state.focusY);
this.props.onClose();
}
setRef = c => {
this.node = c;
}
handleTextDetection = () => {
const { media } = this.props;
this.setState({ detecting: true });
fetchTesseract().then(({ createWorker }) => {
const worker = createWorker({
workerPath: tesseractWorkerPath,
corePath: tesseractCorePath,
langPath: `${assetHost}/ocr/lang-data/`,
logger: ({ status, progress }) => {
if (status === 'recognizing text') {
this.setState({ ocrStatus: 'detecting', progress });
} else {
this.setState({ ocrStatus: 'preparing', progress });
}
},
});
let media_url = media.get('url');
if (window.URL && URL.createObjectURL) {
try {
media_url = URL.createObjectURL(media.get('file'));
} catch (error) {
console.error(error);
}
}
(async () => {
await worker.load();
await worker.loadLanguage('eng');
await worker.initialize('eng');
const { data: { text } } = await worker.recognize(media_url);
this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false });
await worker.terminate();
})();
}).catch((e) => {
console.error(e);
this.setState({ detecting: false });
});
}
handleThumbnailChange = e => {
if (e.target.files.length > 0) {
this.setState({ dirty: true });
this.props.onSelectThumbnail(e.target.files);
}
}
setFileInputRef = c => {
this.fileInput = c;
}
handleFileInputClick = () => {
this.fileInput.click();
}
render () {
const { media, intl, account, onClose, isUploadingThumbnail } = this.props;
const { x, y, dragging, description, dirty, detecting, progress, ocrStatus } = this.state;
const width = media.getIn(['meta', 'original', 'width']) || null;
const height = media.getIn(['meta', 'original', 'height']) || null;
const focals = ['image', 'gifv'].includes(media.get('type'));
const thumbnailable = ['audio', 'video'].includes(media.get('type'));
const previewRatio = 16/9;
const previewWidth = 200;
const previewHeight = previewWidth / previewRatio;
let descriptionLabel = null;
if (media.get('type') === 'audio') {
descriptionLabel =