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,
46 import { Icon, Spinner } from "../common/icon";
47 import { MarkdownTextArea } from "../common/markdown-textarea";
48 import { PostListings } from "./post-listings";
52 Choices = require("choices.js");
55 const MAX_POST_TITLE_LENGTH = 200;
57 interface PostFormProps {
58 post_view?: PostView; // If a post is given, that means this is an edit
59 communities?: CommunityView[];
60 params?: PostFormParams;
62 onCreate?(post: PostView): any;
63 onEdit?(post: PostView): any;
65 enableDownvotes: boolean;
68 interface PostFormState {
71 imageLoading: boolean;
73 suggestedTitle: string;
74 suggestedPosts: PostView[];
75 crossPosts: PostView[];
78 export class PostForm extends Component<PostFormProps, PostFormState> {
79 private subscription: Subscription;
81 private emptyState: PostFormState = {
86 auth: authField(false),
91 suggestedTitle: undefined,
96 constructor(props: any, context: any) {
97 super(props, context);
98 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
99 this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
100 this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
102 this.state = this.emptyState;
105 if (this.props.post_view) {
106 this.state.postForm = {
107 body: this.props.post_view.post.body,
108 name: this.props.post_view.post.name,
109 community_id: this.props.post_view.community.id,
110 url: this.props.post_view.post.url,
111 nsfw: this.props.post_view.post.nsfw,
116 if (this.props.params) {
117 this.state.postForm.name = this.props.params.name;
118 if (this.props.params.url) {
119 this.state.postForm.url = this.props.params.url;
121 if (this.props.params.body) {
122 this.state.postForm.body = this.props.params.body;
126 this.parseMessage = this.parseMessage.bind(this);
127 this.subscription = wsSubscribe(this.parseMessage);
130 componentDidMount() {
132 this.setupCommunities();
133 let textarea: any = document.getElementById("post-title");
139 componentDidUpdate() {
141 !this.state.loading &&
142 (this.state.postForm.name ||
143 this.state.postForm.url ||
144 this.state.postForm.body)
146 window.onbeforeunload = () => true;
148 window.onbeforeunload = undefined;
152 componentWillUnmount() {
153 this.subscription.unsubscribe();
154 /* this.choices && this.choices.destroy(); */
155 window.onbeforeunload = null;
163 !this.state.loading &&
164 (this.state.postForm.name ||
165 this.state.postForm.url ||
166 this.state.postForm.body)
168 message={i18n.t("block_leaving")}
170 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
171 <div class="form-group row">
172 <label class="col-sm-2 col-form-label" htmlFor="post-url">
175 <div class="col-sm-10">
180 value={this.state.postForm.url}
181 onInput={linkEvent(this, this.handlePostUrlChange)}
182 onPaste={linkEvent(this, this.handleImageUploadPaste)}
184 {this.state.suggestedTitle && (
186 class="mt-1 text-muted small font-weight-bold pointer"
188 onClick={linkEvent(this, this.copySuggestedTitle)}
190 {i18n.t("copy_suggested_title", {
191 title: this.state.suggestedTitle,
197 htmlFor="file-upload"
199 UserService.Instance.myUserInfo && "pointer"
200 } d-inline-block float-right text-muted font-weight-bold`}
201 data-tippy-content={i18n.t("upload_image")}
203 <Icon icon="image" classes="icon-inline" />
208 accept="image/*,video/*"
211 disabled={!UserService.Instance.myUserInfo}
212 onChange={linkEvent(this, this.handleImageUpload)}
215 {this.state.postForm.url && validURL(this.state.postForm.url) && (
218 href={`${webArchiveUrl}/save/${encodeURIComponent(
219 this.state.postForm.url
221 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
224 archive.org {i18n.t("archive_link")}
227 href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
228 this.state.postForm.url
230 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
233 ghostarchive.org {i18n.t("archive_link")}
236 href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
237 this.state.postForm.url
239 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
242 archive.today {i18n.t("archive_link")}
246 {this.state.imageLoading && <Spinner />}
247 {isImage(this.state.postForm.url) && (
248 <img src={this.state.postForm.url} class="img-fluid" alt="" />
250 {this.state.crossPosts.length > 0 && (
252 <div class="my-1 text-muted small font-weight-bold">
253 {i18n.t("cross_posts")}
257 posts={this.state.crossPosts}
258 enableDownvotes={this.props.enableDownvotes}
259 enableNsfw={this.props.enableNsfw}
265 <div class="form-group row">
266 <label class="col-sm-2 col-form-label" htmlFor="post-title">
269 <div class="col-sm-10">
271 value={this.state.postForm.name}
273 onInput={linkEvent(this, this.handlePostNameChange)}
274 class={`form-control ${
275 !validTitle(this.state.postForm.name) && "is-invalid"
280 maxLength={MAX_POST_TITLE_LENGTH}
282 {!validTitle(this.state.postForm.name) && (
283 <div class="invalid-feedback">
284 {i18n.t("invalid_post_title")}
287 {this.state.suggestedPosts.length > 0 && (
289 <div class="my-1 text-muted small font-weight-bold">
290 {i18n.t("related_posts")}
293 posts={this.state.suggestedPosts}
294 enableDownvotes={this.props.enableDownvotes}
295 enableNsfw={this.props.enableNsfw}
302 <div class="form-group row">
303 <label class="col-sm-2 col-form-label">{i18n.t("body")}</label>
304 <div class="col-sm-10">
306 initialContent={this.state.postForm.body}
307 onContentChange={this.handlePostBodyChange}
311 {!this.props.post_view && (
312 <div class="form-group row">
313 <label class="col-sm-2 col-form-label" htmlFor="post-community">
314 {i18n.t("community")}
316 <div class="col-sm-10">
320 value={this.state.postForm.community_id}
321 onInput={linkEvent(this, this.handlePostCommunityChange)}
323 <option>{i18n.t("select_a_community")}</option>
324 {this.props.communities.map(cv => (
325 <option value={cv.community.id}>
326 {communitySelectName(cv)}
333 {this.props.enableNsfw && (
334 <div class="form-group row">
335 <legend class="col-form-label col-sm-2 pt-0">
338 <div class="col-sm-10">
339 <div class="form-check">
341 class="form-check-input position-static"
344 checked={this.state.postForm.nsfw}
345 onChange={linkEvent(this, this.handlePostNsfwChange)}
356 class="form-control honeypot"
358 value={this.state.postForm.honeypot}
359 onInput={linkEvent(this, this.handleHoneyPotChange)}
361 <div class="form-group row">
362 <div class="col-sm-10">
365 !this.state.postForm.community_id || this.state.loading
368 class="btn btn-secondary mr-2"
370 {this.state.loading ? (
372 ) : this.props.post_view ? (
373 capitalizeFirstLetter(i18n.t("save"))
375 capitalizeFirstLetter(i18n.t("create"))
378 {this.props.post_view && (
381 class="btn btn-secondary"
382 onClick={linkEvent(this, this.handleCancel)}
394 handlePostSubmit(i: PostForm, event: any) {
395 event.preventDefault();
397 // Coerce empty url string to undefined
398 if (i.state.postForm.url !== undefined && i.state.postForm.url === "") {
399 i.state.postForm.url = undefined;
402 if (i.props.post_view) {
403 let form: EditPost = {
405 post_id: i.props.post_view.post.id,
407 WebSocketService.Instance.send(wsClient.editPost(form));
409 WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
411 i.state.loading = true;
415 copySuggestedTitle(i: PostForm) {
416 i.state.postForm.name = i.state.suggestedTitle.substring(
418 MAX_POST_TITLE_LENGTH
420 i.state.suggestedTitle = undefined;
422 let textarea: any = document.getElementById("post-title");
423 autosize.update(textarea);
428 handlePostUrlChange(i: PostForm, event: any) {
429 i.state.postForm.url = event.target.value;
435 if (validURL(this.state.postForm.url)) {
437 q: this.state.postForm.url,
438 type_: SearchType.Url,
439 sort: SortType.TopAll,
440 listing_type: ListingType.All,
443 auth: authField(false),
446 WebSocketService.Instance.send(wsClient.search(form));
448 // Fetch the page title
449 getSiteMetadata(this.state.postForm.url).then(d => {
450 this.state.suggestedTitle = d.metadata.title;
451 this.setState(this.state);
454 this.state.suggestedTitle = undefined;
455 this.state.crossPosts = [];
459 handlePostNameChange(i: PostForm, event: any) {
460 i.state.postForm.name = event.target.value;
462 i.fetchSimilarPosts();
465 fetchSimilarPosts() {
467 q: this.state.postForm.name,
468 type_: SearchType.Posts,
469 sort: SortType.TopAll,
470 listing_type: ListingType.All,
471 community_id: this.state.postForm.community_id,
474 auth: authField(false),
477 if (this.state.postForm.name !== "") {
478 WebSocketService.Instance.send(wsClient.search(form));
480 this.state.suggestedPosts = [];
483 this.setState(this.state);
486 handlePostBodyChange(val: string) {
487 this.state.postForm.body = val;
488 this.setState(this.state);
491 handlePostCommunityChange(i: PostForm, event: any) {
492 i.state.postForm.community_id = Number(event.target.value);
496 handlePostNsfwChange(i: PostForm, event: any) {
497 i.state.postForm.nsfw = event.target.checked;
501 handleHoneyPotChange(i: PostForm, event: any) {
502 i.state.postForm.honeypot = event.target.value;
506 handleCancel(i: PostForm) {
510 handlePreviewToggle(i: PostForm, event: any) {
511 event.preventDefault();
512 i.state.previewMode = !i.state.previewMode;
516 handleImageUploadPaste(i: PostForm, event: any) {
517 let image = event.clipboardData.files[0];
519 i.handleImageUpload(i, image);
523 handleImageUpload(i: PostForm, event: any) {
526 event.preventDefault();
527 file = event.target.files[0];
532 const formData = new FormData();
533 formData.append("images[]", file);
535 i.state.imageLoading = true;
542 .then(res => res.json())
544 console.log("pictrs upload:");
546 if (res.msg == "ok") {
547 let hash = res.files[0].file;
548 let url = `${pictrsUri}/${hash}`;
549 let deleteToken = res.files[0].delete_token;
550 let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
551 i.state.postForm.url = url;
552 i.state.imageLoading = false;
555 i18n.t("click_to_delete_picture"),
556 i18n.t("picture_deleted"),
560 i.state.imageLoading = false;
562 toast(JSON.stringify(res), "danger");
566 i.state.imageLoading = false;
568 toast(error, "danger");
573 // Set up select searching
575 let selectId: any = document.getElementById("post-community");
577 this.choices = new Choices(selectId, choicesConfig);
578 this.choices.passedElement.element.addEventListener(
581 this.state.postForm.community_id = Number(e.detail.choice.value);
582 this.setState(this.state);
586 this.choices.passedElement.element.addEventListener(
588 debounce(async (e: any) => {
590 let communities = (await fetchCommunities(e.detail.value))
592 this.choices.setChoices(
593 communities.map(cv => communityToChoice(cv)),
607 if (this.props.post_view) {
608 this.state.postForm.community_id = this.props.post_view.community.id;
611 (this.props.params.community_id || this.props.params.community_name)
613 if (this.props.params.community_name) {
614 let foundCommunityId = this.props.communities.find(
615 r => r.community.name == this.props.params.community_name
617 this.state.postForm.community_id = foundCommunityId;
618 } else if (this.props.params.community_id) {
619 this.state.postForm.community_id = this.props.params.community_id;
623 this.choices.setChoiceByValue(
624 this.state.postForm.community_id.toString()
627 this.setState(this.state);
629 // By default, the null valued 'Select a Community'
633 parseMessage(msg: any) {
634 let op = wsUserOp(msg);
637 toast(i18n.t(msg.error), "danger");
638 this.state.loading = false;
639 this.setState(this.state);
641 } else if (op == UserOperation.CreatePost) {
642 let data = wsJsonToRes<PostResponse>(msg).data;
644 data.post_view.creator.id ==
645 UserService.Instance.myUserInfo.local_user_view.person.id
647 this.state.loading = false;
648 this.props.onCreate(data.post_view);
650 } else if (op == UserOperation.EditPost) {
651 let data = wsJsonToRes<PostResponse>(msg).data;
653 data.post_view.creator.id ==
654 UserService.Instance.myUserInfo.local_user_view.person.id
656 this.state.loading = false;
657 this.props.onEdit(data.post_view);
659 } else if (op == UserOperation.Search) {
660 let data = wsJsonToRes<SearchResponse>(msg).data;
662 if (data.type_ == SearchType[SearchType.Posts]) {
663 this.state.suggestedPosts = data.posts;
664 } else if (data.type_ == SearchType[SearchType.Url]) {
665 this.state.crossPosts = data.posts;
667 this.setState(this.state);