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 =