import React from "react" import * as antd from "antd" import classnames from "classnames" import { DateTime } from "luxon" import lodash from "lodash" import humanSize from "@tsmx/human-readable" import PostLink from "@components/PostLink" import { Icons } from "@components/Icons" import Poll from "@components/Poll" import clipboardEventFileToFile from "@utils/clipboardEventFileToFile" import PostModel from "@models/post" import SearchModel from "@models/search" import "./index.less" const DEFAULT_POST_POLICY = { maxMessageLength: 512, acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp", "video/*"], maximunFilesPerRequest: 10 } export default class PostCreator extends React.Component { state = { pending: [], loading: false, uploaderVisible: false, postMessage: "", postAttachments: [], postPoll: null, fileList: [], postingPolicy: DEFAULT_POST_POLICY, mentionsLoadedData: [] } pollRef = React.createRef() creatorRef = React.createRef() cleanPostData = () => { this.setState({ postMessage: "", postAttachments: [], fileList: [] }) } toggleUploaderVisibility = (to) => { to = to ?? !this.state.uploaderVisible if (to === this.state.uploaderVisible) { return } this.setState({ uploaderVisible: to }) } canSubmit = () => { const { postMessage, postAttachments, pending, postingPolicy } = this.state const messageLengthValid = postMessage.length !== 0 && postMessage.length < postingPolicy.maxMessageLength if (pending.length !== 0) { return false } if (!messageLengthValid && postAttachments.length === 0) { return false } return true } submit = lodash.debounce(async () => { if (this.state.loading) { return false } if (!this.canSubmit()) { return false } await this.setState({ loading: true, uploaderVisible: false }) const { postMessage, postAttachments } = this.state const payload = { message: postMessage, attachments: postAttachments, timestamp: DateTime.local().toISO(), } if (this.pollRef.current) { let { options } = this.pollRef.current.getFieldsValue() payload.poll_options = options.filter((option) => !!option.label) } let response = null if (this.props.reply_to) { payload.reply_to = this.props.reply_to } if (this.props.edit_post) { response = await PostModel.update(this.props.edit_post, payload).catch(error => { console.error(error) antd.message.error(error) return false }) } else { response = await PostModel.create(payload).catch(error => { console.error(error) antd.message.error(error) return false }) } this.setState({ loading: false }) if (response) { this.cleanPostData() if (typeof this.props.onPost === "function") { this.props.onPost() } if (typeof this.props.close === "function") { this.props.close() } if (this.props.reply_to) { app.navigation.goToPost(this.props.reply_to) } } }, 50) uploadFile = async (req) => { this.toggleUploaderVisibility(false) const request = await app.cores.remoteStorage.uploadFile(req.file) .catch(error => { console.error(error) antd.message.error(error) req.onError(error) return false }) if (request) { console.log(`Upload done >`, request) return req.onSuccess(request) } } removeAttachment = (file_uid) => { this.setState({ postAttachments: this.state.postAttachments.filter((file) => file.uid !== file_uid), fileList: this.state.fileList.filter((file) => file.uid !== file_uid) }) } addAttachment = (file) => { if (Array.isArray(file)) { return this.setState({ postAttachments: [...this.state.postAttachments, ...file] }) } return this.setState({ postAttachments: [...this.state.postAttachments, file] }) } uploaderScrollToEnd = () => { // scroll to max right const element = document.querySelector(".ant-upload-list-picture-card") // calculate the element's width and scroll to the end const scrollToLeft = element.scrollWidth - element.clientWidth if (element) { element.scrollTo({ top: 0, left: scrollToLeft, behavior: "smooth", }) } } onUploaderChange = (change) => { if (this.state.fileList !== change.fileList) { this.setState({ fileList: change.fileList }) } switch (change.file.status) { case "uploading": { this.toggleUploaderVisibility(false) this.setState({ pending: [...this.state.pending, change.file.uid] }) this.uploaderScrollToEnd() break } case "done": { // remove pending file this.setState({ pending: this.state.pending.filter(uid => uid !== change.file.uid) }) if (Array.isArray(change.file.response.files)) { change.file.response.files.forEach((file) => { this.addAttachment(file) }) } else { this.addAttachment(change.file.response) } // scroll to end this.uploaderScrollToEnd() break } case "error": { // remove pending file this.setState({ pending: this.state.pending.filter(uid => uid !== change.file.uid) }) // remove file from list this.removeAttachment(change.file.uid) } default: { break } } } handleMessageInputChange = (inputText) => { // if the fist character is a space or a whitespace remove it if (inputText[0] === " " || inputText[0] === "\n") { inputText = inputText.slice(1) } this.setState({ postMessage: inputText }) } handleMessageInputKeydown = async (e) => { // detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed if (e.keyCode === 13 && !e.shiftKey) { e.preventDefault() e.stopPropagation() if (this.state.loading) { return false } return await this.debounceSubmit() } } handleOnMentionSearch = lodash.debounce(async (value) => { const results = await SearchModel.userSearch(`username:${value}`) this.setState({ mentionsLoadedData: results }) }, 300) updateFileList = (uid, newValue) => { let updatedFileList = this.state.fileList // find the file in the list const index = updatedFileList.findIndex(file => file.uid === uid) // update the file updatedFileList[index] = newValue // update the state this.setState({ fileList: updatedFileList }) return updatedFileList } handleManualUpload = async (file) => { if (!file) { throw new Error(`No file provided`) } const isValidFormat = (fileType) => { return this.state.postingPolicy.acceptedMimeTypes.includes(fileType) } if (!isValidFormat(file.type)) { throw new Error(`Invalid file format`) } file.thumbUrl = URL.createObjectURL(file) file.uid = `${file.name}-${Math.random() * 1000}` file.status = "uploading" // add file to the uploader this.onUploaderChange({ file, fileList: [...this.state.fileList, file], }) // upload the file await this.uploadFile({ file, onSuccess: (response) => { file.status = "done" file.response = response this.onUploaderChange({ file: file, fileList: this.updateFileList(file.uid, file) }) }, onError: (error) => { file.status = "error" file.error = error this.onUploaderChange({ file: file, fileList: this.updateFileList(file.uid, file) }) } }) return file } handlePaste = async ({ clipboardData }) => { if (clipboardData && clipboardData.items.length > 0) { // check if the clipboard contains a file const hasFile = Array.from(clipboardData.items).some(item => item.kind === "file") if (!hasFile) { return false } for (let index = 0; index < clipboardData.items.length; index++) { const item = clipboardData.items[index] let file = await clipboardEventFileToFile(item).catch((error) => { console.error(error) app.message.error(`Failed to upload file:`, error.message) return false }) this.handleManualUpload(file).catch((error) => { console.error(error) return false }) } } } renderUploadPreviewItem = (item, file, list, actions) => { const uploading = file.status === "uploading" const onClickDelete = () => { this.removeAttachment(file.uid) } return
{ uploading && } { !uploading && } /> }
} handleDrag = (event) => { event.preventDefault() event.stopPropagation() console.log(event) if (event.type === "dragenter") { this.toggleUploaderVisibility(true) } else if (event.type === "dragleave") { // check if mouse is over the uploader or outside the creatorRef if (this.state.uploaderVisible && !this.creatorRef.current.contains(event.target)) { this.toggleUploaderVisibility(false) } } } handleUploadClick = () => { // create a new dialog const dialog = document.createElement("input") // set the dialog type to file dialog.type = "file" // set the dialog accept to the accepted files dialog.accept = this.state.postingPolicy.acceptedMimeTypes dialog.multiple = true // add a listener to the dialog dialog.addEventListener("change", (event) => { // get the files const files = event.target.files // loop through the files for (let index = 0; index < files.length; index++) { const file = files[index] this.handleManualUpload(file).catch((error) => { console.error(error) return false }) } }) // click the dialog dialog.click() } handleAddPoll = () => { if (!this.state.postPoll) { this.setState({ postPoll: [] }) } } handleDeletePoll = () => { this.setState({ postPoll: null }) } componentDidMount = async () => { if (this.props.edit_post) { await this.setState({ loading: true, postId: this.props.edit_post, }) const post = await PostModel.getPost({ post_id: this.props.edit_post }) await this.setState({ loading: false, postMessage: post.message, postAttachments: post.attachments.map((attachment) => { return { ...attachment, uid: attachment.id, } }), fileList: post.attachments.map((attachment) => { return { ...attachment, uid: attachment.id, id: attachment.id, thumbUrl: attachment.url, status: "done", } }), postPoll: post.poll_options }) } // fetch the posting policy //this.fetchUploadPolicy() // add a listener to the window document.addEventListener("paste", this.handlePaste) } componentWillUnmount() { document.removeEventListener("paste", this.handlePaste) } componentDidUpdate(prevProps, prevState) { // if pending is not empty and is not loading if (this.state.pending.length > 0 && !this.state.loading) { this.setState({ loading: true }) } else if (this.state.pending.length === 0 && this.state.loading) { this.setState({ loading: false }) } } render() { const { postMessage, fileList, loading, uploaderVisible, postingPolicy } = this.state const editMode = !!this.props.edit_post const showHeader = !!this.props.edit_post || this.props.reply_to return
{ showHeader &&
{ this.props.edit_post &&

Editing post

} { this.props.reply_to &&

Replaying to

{ this.props.close() app.navigation.goToPost(this.props.reply_to) }} />
}
}
{ return { key: item.id, value: item.username, label: <> {item.username} , } })} onSearch={this.handleOnMentionSearch} />
: (editMode ? : )} />

Drag and drop files here

Max {humanSize.fromBytes(postingPolicy.maximumFileSize)}
{ this.state.postPoll && }
} /> } onClick={this.handleAddPoll} />
} }