import { communityToChoice, fetchCommunities, myAuth, myAuthRequired, } from "@utils/app"; import { capitalizeFirstLetter, debounce, getIdFromString, validTitle, validURL, } from "@utils/helpers"; import { isImage } from "@utils/media"; import { Choice } from "@utils/types"; import autosize from "autosize"; import { Component, InfernoNode, linkEvent } from "inferno"; import { Prompt } from "inferno-router"; import { CommunityView, CreatePost, EditPost, GetSiteMetadataResponse, Language, PostView, SearchResponse, } from "lemmy-js-client"; import { archiveTodayUrl, ghostArchiveUrl, relTags, trendingFetchLimit, webArchiveUrl, } from "../../config"; import { PostFormParams } from "../../interfaces"; import { I18NextService, UserService } from "../../services"; import { HttpService, RequestState } from "../../services/HttpService"; import { setupTippy } from "../../tippy"; import { toast } from "../../toast"; import { Icon, Spinner } from "../common/icon"; import { LanguageSelect } from "../common/language-select"; import { MarkdownTextArea } from "../common/markdown-textarea"; import { SearchableSelect } from "../common/searchable-select"; import { PostListings } from "./post-listings"; const MAX_POST_TITLE_LENGTH = 200; interface PostFormProps { post_view?: PostView; // If a post is given, that means this is an edit crossPosts?: PostView[]; allLanguages: Language[]; siteLanguages: number[]; params?: PostFormParams; onCancel?(): void; onCreate?(form: CreatePost): void; onEdit?(form: EditPost): void; enableNsfw?: boolean; enableDownvotes?: boolean; selectedCommunityChoice?: Choice; onSelectCommunity?: (choice: Choice) => void; initialCommunities?: CommunityView[]; } interface PostFormState { form: { name?: string; url?: string; body?: string; nsfw?: boolean; language_id?: number; community_id?: number; honeypot?: string; }; loading: boolean; suggestedPostsRes: RequestState; metadataRes: RequestState; imageLoading: boolean; imageDeleteUrl: string; communitySearchLoading: boolean; communitySearchOptions: Choice[]; previewMode: boolean; submitted: boolean; } function handlePostSubmit(i: PostForm, event: any) { event.preventDefault(); // Coerce empty url string to undefined if ((i.state.form.url ?? "") === "") { i.setState(s => ((s.form.url = undefined), s)); } i.setState({ loading: true, submitted: true }); const auth = myAuthRequired(); const pForm = i.state.form; const pv = i.props.post_view; if (pv) { i.props.onEdit?.({ name: pForm.name, url: pForm.url, body: pForm.body, nsfw: pForm.nsfw, post_id: pv.post.id, language_id: pForm.language_id, auth, }); } else if (pForm.name && pForm.community_id) { i.props.onCreate?.({ name: pForm.name, community_id: pForm.community_id, url: pForm.url, body: pForm.body, nsfw: pForm.nsfw, language_id: pForm.language_id, honeypot: pForm.honeypot, auth, }); } } function copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) { const sTitle = d.suggestedTitle; if (sTitle) { d.i.setState( s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s), ); d.i.setState({ suggestedPostsRes: { state: "empty" } }); setTimeout(() => { const textarea: any = document.getElementById("post-title"); autosize.update(textarea); }, 10); } } function handlePostUrlChange(i: PostForm, event: any) { const url = event.target.value; i.setState(prev => ({ ...prev, form: { ...prev.form, url, }, imageDeleteUrl: "", })); i.fetchPageTitle(); } function handlePostNsfwChange(i: PostForm, event: any) { i.setState(s => ((s.form.nsfw = event.target.checked), s)); } function handleHoneyPotChange(i: PostForm, event: any) { i.setState(s => ((s.form.honeypot = event.target.value), s)); } function handleCancel(i: PostForm) { i.props.onCancel?.(); } function handleImageUploadPaste(i: PostForm, event: any) { const image = event.clipboardData.files[0]; if (image) { handleImageUpload(i, image); } } function handleImageUpload(i: PostForm, event: any) { let file: any; if (event.target) { event.preventDefault(); file = event.target.files[0]; } else { file = event; } i.setState({ imageLoading: true }); HttpService.client.uploadImage({ image: file }).then(res => { console.log("pictrs upload:"); console.log(res); if (res.state === "success") { if (res.data.msg === "ok") { i.state.form.url = res.data.url; i.setState({ imageLoading: false, imageDeleteUrl: res.data.delete_url as string, }); } else if (res.data.msg === "too_large") { toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } } else if (res.state === "failed") { console.error(res.msg); toast(res.msg, "danger"); i.setState({ imageLoading: false }); } }); } function handlePostNameChange(i: PostForm, event: any) { i.setState(s => ((s.form.name = event.target.value), s)); i.fetchSimilarPosts(); } function handleImageDelete(i: PostForm) { const { imageDeleteUrl } = i.state; fetch(imageDeleteUrl); i.setState(prev => ({ ...prev, imageDeleteUrl: "", imageLoading: false, form: { ...prev.form, url: "", }, })); } export class PostForm extends Component { state: PostFormState = { suggestedPostsRes: { state: "empty" }, metadataRes: { state: "empty" }, form: {}, loading: false, imageLoading: false, imageDeleteUrl: "", communitySearchLoading: false, previewMode: false, communitySearchOptions: [], submitted: false, }; constructor(props: PostFormProps, context: any) { super(props, context); this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this)); this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this)); this.handlePostBodyChange = this.handlePostBodyChange.bind(this); this.handleLanguageChange = this.handleLanguageChange.bind(this); this.handleCommunitySelect = this.handleCommunitySelect.bind(this); const { post_view, selectedCommunityChoice, params } = this.props; // Means its an edit if (post_view) { this.state = { ...this.state, form: { body: post_view.post.body, name: post_view.post.name, community_id: post_view.community.id, url: post_view.post.url, nsfw: post_view.post.nsfw, language_id: post_view.post.language_id, }, }; } else if (selectedCommunityChoice) { this.state = { ...this.state, form: { ...this.state.form, community_id: getIdFromString(selectedCommunityChoice.value), }, communitySearchOptions: [selectedCommunityChoice].concat( ( this.props.initialCommunities?.map( ({ community: { id, title } }) => ({ label: title, value: id.toString(), }), ) ?? [] ).filter(option => option.value !== selectedCommunityChoice.value), ), }; } else { this.state = { ...this.state, communitySearchOptions: this.props.initialCommunities?.map( ({ community: { id, title } }) => ({ label: title, value: id.toString(), }), ) ?? [], }; } if (params) { this.state = { ...this.state, form: { ...this.state.form, ...params, }, }; } } componentDidMount() { setupTippy(); const textarea: any = document.getElementById("post-title"); if (textarea) { autosize(textarea); } } componentWillReceiveProps( nextProps: Readonly<{ children?: InfernoNode } & PostFormProps>, ): void { if (this.props !== nextProps) { this.setState( s => ( (s.form.community_id = getIdFromString( nextProps.selectedCommunityChoice?.value, )), s ), ); } } render() { const firstLang = this.state.form.language_id; const selectedLangs = firstLang ? Array.of(firstLang) : undefined; const url = this.state.form.url; return (