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,
47 import { Icon, Spinner } from "../common/icon";
48 import { MarkdownTextArea } from "../common/markdown-textarea";
49 import { PostListings } from "./post-listings";
53 Choices = require("choices.js");
56 const MAX_POST_TITLE_LENGTH = 200;
58 interface PostFormProps {
59 post_view?: PostView; // If a post is given, that means this is an edit
60 communities?: CommunityView[];
61 params?: PostFormParams;
63 onCreate?(post: PostView): any;
64 onEdit?(post: PostView): any;
66 enableDownvotes: boolean;
69 interface PostFormState {
72 imageLoading: boolean;
74 suggestedTitle: string;
75 suggestedPosts: PostView[];
76 crossPosts: PostView[];
79 export class PostForm extends Component<PostFormProps, PostFormState> {
80 private subscription: Subscription;
82 private emptyState: PostFormState = {
87 auth: authField(false),
92 suggestedTitle: undefined,
97 constructor(props: any, context: any) {
98 super(props, context);
99 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
100 this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
101 this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
103 this.state = this.emptyState;
106 if (this.props.post_view) {
107 this.state.postForm = {
108 body: this.props.post_view.post.body,
109 name: this.props.post_view.post.name,
110 community_id: this.props.post_view.community.id,
111 url: this.props.post_view.post.url,
112 nsfw: this.props.post_view.post.nsfw,
117 if (this.props.params) {
118 this.state.postForm.name = this.props.params.name;
119 if (this.props.params.url) {
120 this.state.postForm.url = this.props.params.url;
122 if (this.props.params.body) {
123 this.state.postForm.body = this.props.params.body;
127 this.parseMessage = this.parseMessage.bind(this);
128 this.subscription = wsSubscribe(this.parseMessage);
131 componentDidMount() {
133 this.setupCommunities();
134 let textarea: any = document.getElementById("post-title");
140 componentDidUpdate() {
142 !this.state.loading &&
143 (this.state.postForm.name ||
144 this.state.postForm.url ||
145 this.state.postForm.body)
147 window.onbeforeunload = () => true;
149 window.onbeforeunload = undefined;
153 componentWillUnmount() {
154 this.subscription.unsubscribe();
155 /* this.choices && this.choices.destroy(); */
156 window.onbeforeunload = null;
164 !this.state.loading &&
165 (this.state.postForm.name ||
166 this.state.postForm.url ||
167 this.state.postForm.body)
169 message={i18n.t("block_leaving")}
171 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
172 <div class="form-group row">
173 <label class="col-sm-2 col-form-label" htmlFor="post-url">
176 <div class="col-sm-10">
181 value={this.state.postForm.url}
182 onInput={linkEvent(this, this.handlePostUrlChange)}
183 onPaste={linkEvent(this, this.handleImageUploadPaste)}
185 {this.state.suggestedTitle && (
187 class="mt-1 text-muted small font-weight-bold pointer"
189 onClick={linkEvent(this, this.copySuggestedTitle)}
191 {i18n.t("copy_suggested_title", {
192 title: this.state.suggestedTitle,
198 htmlFor="file-upload"
200 UserService.Instance.myUserInfo && "pointer"
201 } d-inline-block float-right text-muted font-weight-bold`}
202 data-tippy-content={i18n.t("upload_image")}
204 <Icon icon="image" classes="icon-inline" />
209 accept="image/*,video/*"
212 disabled={!UserService.Instance.myUserInfo}
213 onChange={linkEvent(this, this.handleImageUpload)}
216 {this.state.postForm.url && validURL(this.state.postForm.url) && (
219 href={`${webArchiveUrl}/save/${encodeURIComponent(
220 this.state.postForm.url
222 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
225 archive.org {i18n.t("archive_link")}
228 href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
229 this.state.postForm.url
231 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
234 ghostarchive.org {i18n.t("archive_link")}
237 href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
238 this.state.postForm.url
240 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
243 archive.today {i18n.t("archive_link")}
247 {this.state.imageLoading && <Spinner />}
248 {isImage(this.state.postForm.url) && (
249 <img src={this.state.postForm.url} class="img-fluid" alt="" />
251 {this.state.crossPosts.length > 0 && (
253 <div class="my-1 text-muted small font-weight-bold">
254 {i18n.t("cross_posts")}
258 posts={this.state.crossPosts}
259 enableDownvotes={this.props.enableDownvotes}
260 enableNsfw={this.props.enableNsfw}
266 <div class="form-group row">
267 <label class="col-sm-2 col-form-label" htmlFor="post-title">
270 <div class="col-sm-10">
272 value={this.state.postForm.name}
274 onInput={linkEvent(this, this.handlePostNameChange)}
275 class={`form-control ${
276 !validTitle(this.state.postForm.name) && "is-invalid"
281 maxLength={MAX_POST_TITLE_LENGTH}
283 {!validTitle(this.state.postForm.name) && (
284 <div class="invalid-feedback">
285 {i18n.t("invalid_post_title")}
288 {this.state.suggestedPosts.length > 0 && (
290 <div class="my-1 text-muted small font-weight-bold">
291 {i18n.t("related_posts")}
294 posts={this.state.suggestedPosts}
295 enableDownvotes={this.props.enableDownvotes}
296 enableNsfw={this.props.enableNsfw}
303 <div class="form-group row">
304 <label class="col-sm-2 col-form-label">{i18n.t("body")}</label>
305 <div class="col-sm-10">
307 initialContent={this.state.postForm.body}
308 onContentChange={this.handlePostBodyChange}
312 {!this.props.post_view && (
313 <div class="form-group row">
314 <label class="col-sm-2 col-form-label" htmlFor="post-community">
315 {i18n.t("community")}
317 <div class="col-sm-10">
321 value={this.state.postForm.community_id}
322 onInput={linkEvent(this, this.handlePostCommunityChange)}
324 <option>{i18n.t("select_a_community")}</option>
325 {this.props.communities.map(cv => (
326 <option value={cv.community.id}>
327 {communitySelectName(cv)}
334 {this.props.enableNsfw && (
335 <div class="form-group row">
336 <legend class="col-form-label col-sm-2 pt-0">
339 <div class="col-sm-10">
340 <div class="form-check">
342 class="form-check-input position-static"
345 checked={this.state.postForm.nsfw}
346 onChange={linkEvent(this, this.handlePostNsfwChange)}
357 class="form-control honeypot"
359 value={this.state.postForm.honeypot}
360 onInput={linkEvent(this, this.handleHoneyPotChange)}
362 <div class="form-group row">
363 <div class="col-sm-10">
366 !this.state.postForm.community_id || this.state.loading
369 class="btn btn-secondary mr-2"
371 {this.state.loading ? (
373 ) : this.props.post_view ? (
374 capitalizeFirstLetter(i18n.t("save"))
376 capitalizeFirstLetter(i18n.t("create"))
379 {this.props.post_view && (
382 class="btn btn-secondary"
383 onClick={linkEvent(this, this.handleCancel)}
395 handlePostSubmit(i: PostForm, event: any) {
396 event.preventDefault();
398 // Coerce empty url string to undefined
399 if (i.state.postForm.url !== undefined && i.state.postForm.url === "") {
400 i.state.postForm.url = undefined;
403 if (i.props.post_view) {
404 let form: EditPost = {
406 post_id: i.props.post_view.post.id,
408 WebSocketService.Instance.send(wsClient.editPost(form));
410 WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
412 i.state.loading = true;
416 copySuggestedTitle(i: PostForm) {
417 i.state.postForm.name = i.state.suggestedTitle.substring(
419 MAX_POST_TITLE_LENGTH
421 i.state.suggestedTitle = undefined;
423 let textarea: any = document.getElementById("post-title");
424 autosize.update(textarea);
429 handlePostUrlChange(i: PostForm, event: any) {
430 i.state.postForm.url = event.target.value;
436 if (validURL(this.state.postForm.url)) {
438 q: this.state.postForm.url,
439 type_: SearchType.Url,
440 sort: SortType.TopAll,
441 listing_type: ListingType.All,
444 auth: authField(false),
447 WebSocketService.Instance.send(wsClient.search(form));
449 // Fetch the page title
450 getSiteMetadata(this.state.postForm.url).then(d => {
451 this.state.suggestedTitle = d.metadata.title;
452 this.setState(this.state);
455 this.state.suggestedTitle = undefined;
456 this.state.crossPosts = [];
460 handlePostNameChange(i: PostForm, event: any) {
461 i.state.postForm.name = event.target.value;
463 i.fetchSimilarPosts();
466 fetchSimilarPosts() {
468 q: this.state.postForm.name,
469 type_: SearchType.Posts,
470 sort: SortType.TopAll,
471 listing_type: ListingType.All,
472 community_id: this.state.postForm.community_id,
475 auth: authField(false),
478 if (this.state.postForm.name !== "") {
479 WebSocketService.Instance.send(wsClient.search(form));
481 this.state.suggestedPosts = [];
484 this.setState(this.state);
487 handlePostBodyChange(val: string) {
488 this.state.postForm.body = val;
489 this.setState(this.state);
492 handlePostCommunityChange(i: PostForm, event: any) {
493 i.state.postForm.community_id = Number(event.target.value);
497 handlePostNsfwChange(i: PostForm, event: any) {
498 i.state.postForm.nsfw = event.target.checked;
502 handleHoneyPotChange(i: PostForm, event: any) {
503 i.state.postForm.honeypot = event.target.value;
507 handleCancel(i: PostForm) {
511 handlePreviewToggle(i: PostForm, event: any) {
512 event.preventDefault();
513 i.state.previewMode = !i.state.previewMode;
517 handleImageUploadPaste(i: PostForm, event: any) {
518 let image = event.clipboardData.files[0];
520 i.handleImageUpload(i, image);
524 handleImageUpload(i: PostForm, event: any) {
527 event.preventDefault();
528 file = event.target.files[0];
533 const formData = new FormData();
534 formData.append("images[]", file);
536 i.state.imageLoading = true;
543 .then(res => res.json())
545 console.log("pictrs upload:");
547 if (res.msg == "ok") {
548 let hash = res.files[0].file;
549 let url = `${pictrsUri}/${hash}`;
550 let deleteToken = res.files[0].delete_token;
551 let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
552 i.state.postForm.url = url;
553 i.state.imageLoading = false;
556 i18n.t("click_to_delete_picture"),
557 i18n.t("picture_deleted"),
561 i.state.imageLoading = false;
563 toast(JSON.stringify(res), "danger");
567 i.state.imageLoading = false;
569 console.error(error);
570 toast(error, "danger");
575 // Set up select searching
577 let selectId: any = document.getElementById("post-community");
579 this.choices = new Choices(selectId, choicesConfig);
580 this.choices.passedElement.element.addEventListener(
583 this.state.postForm.community_id = Number(e.detail.choice.value);
584 this.setState(this.state);
588 this.choices.passedElement.element.addEventListener(
590 debounce(async (e: any) => {
592 let communities = (await fetchCommunities(e.detail.value))
594 this.choices.setChoices(
595 communities.map(cv => communityToChoice(cv)),
609 if (this.props.post_view) {
610 this.state.postForm.community_id = this.props.post_view.community.id;
613 (this.props.params.community_id || this.props.params.community_name)
615 if (this.props.params.community_name) {
616 let foundCommunityId = this.props.communities.find(
617 r => r.community.name == this.props.params.community_name
619 this.state.postForm.community_id = foundCommunityId;
620 } else if (this.props.params.community_id) {
621 this.state.postForm.community_id = this.props.params.community_id;
625 this.choices.setChoiceByValue(
626 this.state.postForm.community_id.toString()
629 this.setState(this.state);
631 // By default, the null valued 'Select a Community'
635 parseMessage(msg: any) {
636 let op = wsUserOp(msg);
639 toast(i18n.t(msg.error), "danger");
640 this.state.loading = false;
641 this.setState(this.state);
643 } else if (op == UserOperation.CreatePost) {
644 let data = wsJsonToRes<PostResponse>(msg).data;
646 data.post_view.creator.id ==
647 UserService.Instance.myUserInfo.local_user_view.person.id
649 this.state.loading = false;
650 this.props.onCreate(data.post_view);
652 } else if (op == UserOperation.EditPost) {
653 let data = wsJsonToRes<PostResponse>(msg).data;
655 data.post_view.creator.id ==
656 UserService.Instance.myUserInfo.local_user_view.person.id
658 this.state.loading = false;
659 this.props.onEdit(data.post_view);
661 } else if (op == UserOperation.Search) {
662 let data = wsJsonToRes<SearchResponse>(msg).data;
664 if (data.type_ == SearchType[SearchType.Posts]) {
665 this.state.suggestedPosts = data.posts;
666 } else if (data.type_ == SearchType[SearchType.Url]) {
667 this.state.crossPosts = data.posts;
669 this.setState(this.state);