1 import autosize from "autosize";
2 import { Component, linkEvent } from "inferno";
3 import { Prompt } from "inferno-router";
16 } from "lemmy-js-client";
17 import { Subscription } from "rxjs";
18 import { pictrsUri } from "../../env";
19 import { i18n } from "../../i18next";
20 import { PostFormParams } from "../../interfaces";
21 import { UserService, WebSocketService } from "../../services";
25 capitalizeFirstLetter,
44 import { Icon, Spinner } from "../common/icon";
45 import { MarkdownTextArea } from "../common/markdown-textarea";
46 import { PostListings } from "./post-listings";
50 Choices = require("choices.js");
53 const MAX_POST_TITLE_LENGTH = 200;
55 interface PostFormProps {
56 post_view?: PostView; // If a post is given, that means this is an edit
57 communities?: CommunityView[];
58 params?: PostFormParams;
60 onCreate?(post: PostView): any;
61 onEdit?(post: PostView): any;
63 enableDownvotes: boolean;
66 interface PostFormState {
69 imageLoading: boolean;
71 suggestedTitle: string;
72 suggestedPosts: PostView[];
73 crossPosts: PostView[];
76 export class PostForm extends Component<PostFormProps, PostFormState> {
77 private subscription: Subscription;
79 private emptyState: PostFormState = {
84 auth: authField(false),
89 suggestedTitle: undefined,
94 constructor(props: any, context: any) {
95 super(props, context);
96 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
97 this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
98 this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
100 this.state = this.emptyState;
103 if (this.props.post_view) {
104 this.state.postForm = {
105 body: this.props.post_view.post.body,
106 name: this.props.post_view.post.name,
107 community_id: this.props.post_view.community.id,
108 url: this.props.post_view.post.url,
109 nsfw: this.props.post_view.post.nsfw,
114 if (this.props.params) {
115 this.state.postForm.name = this.props.params.name;
116 if (this.props.params.url) {
117 this.state.postForm.url = this.props.params.url;
119 if (this.props.params.body) {
120 this.state.postForm.body = this.props.params.body;
124 this.parseMessage = this.parseMessage.bind(this);
125 this.subscription = wsSubscribe(this.parseMessage);
128 componentDidMount() {
130 this.setupCommunities();
131 let textarea: any = document.getElementById("post-title");
137 componentDidUpdate() {
139 !this.state.loading &&
140 (this.state.postForm.name ||
141 this.state.postForm.url ||
142 this.state.postForm.body)
144 window.onbeforeunload = () => true;
146 window.onbeforeunload = undefined;
150 componentWillUnmount() {
151 this.subscription.unsubscribe();
152 /* this.choices && this.choices.destroy(); */
153 window.onbeforeunload = null;
161 !this.state.loading &&
162 (this.state.postForm.name ||
163 this.state.postForm.url ||
164 this.state.postForm.body)
166 message={i18n.t("block_leaving")}
168 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
169 <div class="form-group row">
170 <label class="col-sm-2 col-form-label" htmlFor="post-url">
173 <div class="col-sm-10">
178 value={this.state.postForm.url}
179 onInput={linkEvent(this, this.handlePostUrlChange)}
180 onPaste={linkEvent(this, this.handleImageUploadPaste)}
182 {this.state.suggestedTitle && (
184 class="mt-1 text-muted small font-weight-bold pointer"
186 onClick={linkEvent(this, this.copySuggestedTitle)}
188 {i18n.t("copy_suggested_title", {
189 title: this.state.suggestedTitle,
195 htmlFor="file-upload"
197 UserService.Instance.myUserInfo && "pointer"
198 } d-inline-block float-right text-muted font-weight-bold`}
199 data-tippy-content={i18n.t("upload_image")}
201 <Icon icon="image" classes="icon-inline" />
206 accept="image/*,video/*"
209 disabled={!UserService.Instance.myUserInfo}
210 onChange={linkEvent(this, this.handleImageUpload)}
213 {this.state.postForm.url && validURL(this.state.postForm.url) && (
215 href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
216 this.state.postForm.url
218 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
221 {i18n.t("archive_link")}
224 {this.state.imageLoading && <Spinner />}
225 {isImage(this.state.postForm.url) && (
226 <img src={this.state.postForm.url} class="img-fluid" alt="" />
228 {this.state.crossPosts.length > 0 && (
230 <div class="my-1 text-muted small font-weight-bold">
231 {i18n.t("cross_posts")}
235 posts={this.state.crossPosts}
236 enableDownvotes={this.props.enableDownvotes}
237 enableNsfw={this.props.enableNsfw}
243 <div class="form-group row">
244 <label class="col-sm-2 col-form-label" htmlFor="post-title">
247 <div class="col-sm-10">
249 value={this.state.postForm.name}
251 onInput={linkEvent(this, this.handlePostNameChange)}
252 class={`form-control ${
253 !validTitle(this.state.postForm.name) && "is-invalid"
258 maxLength={MAX_POST_TITLE_LENGTH}
260 {!validTitle(this.state.postForm.name) && (
261 <div class="invalid-feedback">
262 {i18n.t("invalid_post_title")}
265 {this.state.suggestedPosts.length > 0 && (
267 <div class="my-1 text-muted small font-weight-bold">
268 {i18n.t("related_posts")}
271 posts={this.state.suggestedPosts}
272 enableDownvotes={this.props.enableDownvotes}
273 enableNsfw={this.props.enableNsfw}
280 <div class="form-group row">
281 <label class="col-sm-2 col-form-label">{i18n.t("body")}</label>
282 <div class="col-sm-10">
284 initialContent={this.state.postForm.body}
285 onContentChange={this.handlePostBodyChange}
289 {!this.props.post_view && (
290 <div class="form-group row">
291 <label class="col-sm-2 col-form-label" htmlFor="post-community">
292 {i18n.t("community")}
294 <div class="col-sm-10">
298 value={this.state.postForm.community_id}
299 onInput={linkEvent(this, this.handlePostCommunityChange)}
301 <option>{i18n.t("select_a_community")}</option>
302 {this.props.communities.map(cv => (
303 <option value={cv.community.id}>
304 {communitySelectName(cv)}
311 {this.props.enableNsfw && (
312 <div class="form-group row">
313 <div class="col-sm-10">
314 <div class="form-check">
316 class="form-check-input"
319 checked={this.state.postForm.nsfw}
320 onChange={linkEvent(this, this.handlePostNsfwChange)}
322 <label class="form-check-label" htmlFor="post-nsfw">
329 <div class="form-group row">
330 <div class="col-sm-10">
333 !this.state.postForm.community_id || this.state.loading
336 class="btn btn-secondary mr-2"
338 {this.state.loading ? (
340 ) : this.props.post_view ? (
341 capitalizeFirstLetter(i18n.t("save"))
343 capitalizeFirstLetter(i18n.t("create"))
346 {this.props.post_view && (
349 class="btn btn-secondary"
350 onClick={linkEvent(this, this.handleCancel)}
362 handlePostSubmit(i: PostForm, event: any) {
363 event.preventDefault();
365 // Coerce empty url string to undefined
366 if (i.state.postForm.url !== undefined && i.state.postForm.url === "") {
367 i.state.postForm.url = undefined;
370 if (i.props.post_view) {
371 let form: EditPost = {
373 post_id: i.props.post_view.post.id,
375 WebSocketService.Instance.send(wsClient.editPost(form));
377 WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
379 i.state.loading = true;
383 copySuggestedTitle(i: PostForm) {
384 i.state.postForm.name = i.state.suggestedTitle.substring(
386 MAX_POST_TITLE_LENGTH
388 i.state.suggestedTitle = undefined;
390 let textarea: any = document.getElementById("post-title");
391 autosize.update(textarea);
396 handlePostUrlChange(i: PostForm, event: any) {
397 i.state.postForm.url = event.target.value;
403 if (validURL(this.state.postForm.url)) {
405 q: this.state.postForm.url,
406 type_: SearchType.Url,
407 sort: SortType.TopAll,
408 listing_type: ListingType.All,
411 auth: authField(false),
414 WebSocketService.Instance.send(wsClient.search(form));
416 // Fetch the page title
417 getSiteMetadata(this.state.postForm.url).then(d => {
418 this.state.suggestedTitle = d.metadata.title;
419 this.setState(this.state);
422 this.state.suggestedTitle = undefined;
423 this.state.crossPosts = [];
427 handlePostNameChange(i: PostForm, event: any) {
428 i.state.postForm.name = event.target.value;
430 i.fetchSimilarPosts();
433 fetchSimilarPosts() {
435 q: this.state.postForm.name,
436 type_: SearchType.Posts,
437 sort: SortType.TopAll,
438 listing_type: ListingType.All,
439 community_id: this.state.postForm.community_id,
442 auth: authField(false),
445 if (this.state.postForm.name !== "") {
446 WebSocketService.Instance.send(wsClient.search(form));
448 this.state.suggestedPosts = [];
451 this.setState(this.state);
454 handlePostBodyChange(val: string) {
455 this.state.postForm.body = val;
456 this.setState(this.state);
459 handlePostCommunityChange(i: PostForm, event: any) {
460 i.state.postForm.community_id = Number(event.target.value);
464 handlePostNsfwChange(i: PostForm, event: any) {
465 i.state.postForm.nsfw = event.target.checked;
469 handleCancel(i: PostForm) {
473 handlePreviewToggle(i: PostForm, event: any) {
474 event.preventDefault();
475 i.state.previewMode = !i.state.previewMode;
479 handleImageUploadPaste(i: PostForm, event: any) {
480 let image = event.clipboardData.files[0];
482 i.handleImageUpload(i, image);
486 handleImageUpload(i: PostForm, event: any) {
489 event.preventDefault();
490 file = event.target.files[0];
495 const formData = new FormData();
496 formData.append("images[]", file);
498 i.state.imageLoading = true;
505 .then(res => res.json())
507 console.log("pictrs upload:");
509 if (res.msg == "ok") {
510 let hash = res.files[0].file;
511 let url = `${pictrsUri}/${hash}`;
512 let deleteToken = res.files[0].delete_token;
513 let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
514 i.state.postForm.url = url;
515 i.state.imageLoading = false;
518 i18n.t("click_to_delete_picture"),
519 i18n.t("picture_deleted"),
523 i.state.imageLoading = false;
525 toast(JSON.stringify(res), "danger");
529 i.state.imageLoading = false;
531 toast(error, "danger");
536 // Set up select searching
538 let selectId: any = document.getElementById("post-community");
540 this.choices = new Choices(selectId, choicesConfig);
541 this.choices.passedElement.element.addEventListener(
544 this.state.postForm.community_id = Number(e.detail.choice.value);
545 this.setState(this.state);
549 this.choices.passedElement.element.addEventListener(
551 debounce(async (e: any) => {
552 let communities = (await fetchCommunities(e.detail.value))
554 this.choices.setChoices(
555 communities.map(cv => communityToChoice(cv)),
566 if (this.props.post_view) {
567 this.state.postForm.community_id = this.props.post_view.community.id;
570 (this.props.params.community_id || this.props.params.community_name)
572 if (this.props.params.community_name) {
573 let foundCommunityId = this.props.communities.find(
574 r => r.community.name == this.props.params.community_name
576 this.state.postForm.community_id = foundCommunityId;
577 } else if (this.props.params.community_id) {
578 this.state.postForm.community_id = this.props.params.community_id;
582 this.choices.setChoiceByValue(
583 this.state.postForm.community_id.toString()
586 this.setState(this.state);
588 // By default, the null valued 'Select a Community'
592 parseMessage(msg: any) {
593 let op = wsUserOp(msg);
596 toast(i18n.t(msg.error), "danger");
597 this.state.loading = false;
598 this.setState(this.state);
600 } else if (op == UserOperation.CreatePost) {
601 let data = wsJsonToRes<PostResponse>(msg).data;
603 data.post_view.creator.id ==
604 UserService.Instance.myUserInfo.local_user_view.person.id
606 this.state.loading = false;
607 this.props.onCreate(data.post_view);
609 } else if (op == UserOperation.EditPost) {
610 let data = wsJsonToRes<PostResponse>(msg).data;
612 data.post_view.creator.id ==
613 UserService.Instance.myUserInfo.local_user_view.person.id
615 this.state.loading = false;
616 this.props.onEdit(data.post_view);
618 } else if (op == UserOperation.Search) {
619 let data = wsJsonToRes<SearchResponse>(msg).data;
621 if (data.type_ == SearchType[SearchType.Posts]) {
622 this.state.suggestedPosts = data.posts;
623 } else if (data.type_ == SearchType[SearchType.Url]) {
624 this.state.crossPosts = data.posts;
626 this.setState(this.state);