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 } 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 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'; 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' }, }); const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), }); const mapDispatchToProps = (dispatch, { id }) => ({ onSave: (description, x, y) => { dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); }, }); const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') .replace(/\n/g, ' ') .replace(/\*\*\*\*\*\*/g, '\n\n'); const assetHost = process.env.CDN_HOST || ''; export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.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, }; 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 }); } 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(({ TesseractWorker }) => { const worker = new TesseractWorker({ workerPath: `${assetHost}/packs/ocr/worker.min.js`, corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`, langPath: `${assetHost}/ocr/lang-data`, }); worker.recognize(media.get('url')) .progress(({ progress }) => this.setState({ progress })) .finally(() => worker.terminate()) .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false })) .catch(() => this.setState({ detecting: false })); }).catch(() => this.setState({ detecting: false })); } render () { const { media, intl, onClose } = this.props; const { x, y, dragging, description, dirty, detecting, progress } = 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 previewRatio = 16/9; const previewWidth = 200; const previewHeight = previewWidth / previewRatio; return (