import PropTypes from "prop-types"; import { PureComponent } from "react"; import { FormattedMessage, defineMessages, injectIntl } from "react-intl"; import classNames from "classnames"; import ImmutablePropTypes from "react-immutable-proptypes"; import ImmutablePureComponent from "react-immutable-pure-component"; import { connect } from "react-redux"; import Textarea from "react-textarea-autosize"; import { length } from "stringz"; import tesseractWorkerPath from "tesseract.js/dist/worker.min.js"; import tesseractCorePath from "tesseract.js-core/tesseract-core.wasm.js"; import Button from "mastodon/components/button"; import { GIFV } from "mastodon/components/gifv"; import { IconButton } from "mastodon/components/icon_button"; import Audio from "mastodon/features/audio"; import CharacterCounter from "mastodon/features/compose/components/character_counter"; import UploadProgress from "mastodon/features/compose/components/upload_progress"; import { Tesseract as fetchTesseract } from "mastodon/features/ui/util/async-components"; import { me , maxMediaDescChars } from "mastodon/initial_state"; import { assetHost } from "mastodon/utils/config"; import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from "../../../actions/compose"; import Video, { getPointerPosition } from "../../video"; const messages = defineMessages({ close: { id: "lightbox.close", defaultMessage: "Close" }, apply: { id: "upload_modal.apply", defaultMessage: "Apply" }, applying: { id: "upload_modal.applying", defaultMessage: "Applying…" }, 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" }, discardMessage: { id: "confirmations.discard_edit_media.message", defaultMessage: "You have unsaved changes to the media description or preview, discard them anyway?" }, discardConfirm: { id: "confirmations.discard_edit_media.confirm", defaultMessage: "Discard" }, }); 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"]), description: state.getIn(["compose", "media_modal", "description"]), lang: state.getIn(["compose", "language"]), focusX: state.getIn(["compose", "media_modal", "focusX"]), focusY: state.getIn(["compose", "media_modal", "focusY"]), dirty: state.getIn(["compose", "media_modal", "dirty"]), is_changing_upload: state.getIn(["compose", "is_changing_upload"]), }); const mapDispatchToProps = (dispatch, { id }) => ({ onSave: (description, x, y) => { dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); }, onChangeDescription: (description) => { dispatch(onChangeMediaDescription(description)); }, onChangeFocus: (focusX, focusY) => { dispatch(onChangeMediaFocus(focusX, focusY)); }, 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 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 ; } } } class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired, isUploadingThumbnail: PropTypes.bool, onSave: PropTypes.func.isRequired, onChangeDescription: PropTypes.func.isRequired, onChangeFocus: PropTypes.func.isRequired, onSelectThumbnail: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; state = { dragging: false, dirty: false, progress: 0, loading: true, ocrStatus: "", }; 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.props.onChangeFocus(focusX, focusY); }; handleChange = e => { this.props.onChangeDescription(e.target.value); }; handleKeyDown = (e) => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { this.props.onChangeDescription(e.target.value); this.handleSubmit(e); } }; handleSubmit = (e) => { e.preventDefault(); e.stopPropagation(); this.props.onSave(this.props.description, this.props.focusX, this.props.focusY); }; getCloseConfirmationMessage = () => { const { intl, dirty } = this.props; if (dirty) { return { message: intl.formatMessage(messages.discardMessage), confirm: intl.formatMessage(messages.discardConfirm), }; } else { return null; } }; setRef = c => { this.node = c; }; handleTextDetection = () => { this._detectText(); }; _detectText = (refreshCache = false) => { 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 }); } }, cacheMethod: refreshCache ? "refresh" : "write", }); let media_url = media.get("url"); if (window.URL && URL.createObjectURL) { try { media_url = URL.createObjectURL(media.get("file")); } catch (error) { console.error(error); } } return (async () => { await worker.load(); await worker.loadLanguage("eng"); await worker.initialize("eng"); const { data: { text } } = await worker.recognize(media_url); this.setState({ detecting: false }); this.props.onChangeDescription(removeExtraLineBreaks(text)); await worker.terminate(); })().catch((e) => { if (refreshCache) { throw e; } else { this._detectText(true); } }); }).catch((e) => { console.error(e); this.setState({ detecting: false }); }); }; handleThumbnailChange = e => { if (e.target.files.length > 0) { this.props.onSelectThumbnail(e.target.files); } }; setFileInputRef = c => { this.fileInput = c; }; handleFileInputClick = () => { this.fileInput.click(); }; render () { const { media, intl, account, onClose, isUploadingThumbnail, description, lang, focusX, focusY, dirty, is_changing_upload } = this.props; const { dragging, detecting, progress, ocrStatus } = this.state; const x = (focusX / 2) + .5; const y = (focusY / -2) + .5; 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; if (media.get("type") === "audio") { descriptionLabel = ; } else if (media.get("type") === "video") { descriptionLabel = ; } else { descriptionLabel = ; } let ocrMessage; if (ocrStatus === "detecting") { ocrMessage = ; } else { ocrMessage = ; } return (
{focals &&

} {thumbnailable && ( <>