1 import { Component, linkEvent } from 'inferno';
2 import { Prompt } from 'inferno-router';
3 import { PostListings } from './post-listings';
4 import { MarkdownTextArea } from './markdown-textarea';
5 import { Subscription } from 'rxjs';
17 } from 'lemmy-js-client';
18 import { WebSocketService, UserService } from '../services';
19 import { PostFormParams } from '../interfaces';
24 capitalizeFirstLetter,
41 Choices = require('choices.js');
44 import { i18n } from '../i18next';
45 import { pictrsUri } from '../env';
47 const MAX_POST_TITLE_LENGTH = 200;
49 interface PostFormProps {
50 post_view?: PostView; // If a post is given, that means this is an edit
51 communities?: CommunityView[];
52 params?: PostFormParams;
54 onCreate?(post: PostView): any;
55 onEdit?(post: PostView): any;
57 enableDownvotes: boolean;
60 interface PostFormState {
63 imageLoading: boolean;
65 suggestedTitle: string;
66 suggestedPosts: PostView[];
67 crossPosts: PostView[];
70 export class PostForm extends Component<PostFormProps, PostFormState> {
71 private id = `post-form-${randomStr()}`;
72 private subscription: Subscription;
74 private emptyState: PostFormState = {
79 auth: UserService.Instance.authField(),
84 suggestedTitle: undefined,
89 constructor(props: any, context: any) {
90 super(props, context);
91 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
92 this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
93 this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
95 this.state = this.emptyState;
98 if (this.props.post_view) {
99 this.state.postForm = {
100 body: this.props.post_view.post.body,
101 name: this.props.post_view.post.name,
102 community_id: this.props.post_view.community.id,
103 url: this.props.post_view.post.url,
104 nsfw: this.props.post_view.post.nsfw,
105 auth: UserService.Instance.authField(),
109 if (this.props.params) {
110 this.state.postForm.name = this.props.params.name;
111 if (this.props.params.url) {
112 this.state.postForm.url = this.props.params.url;
114 if (this.props.params.body) {
115 this.state.postForm.body = this.props.params.body;
119 this.parseMessage = this.parseMessage.bind(this);
120 this.subscription = wsSubscribe(this.parseMessage);
123 componentDidMount() {
125 this.setupCommunities();
128 componentDidUpdate() {
130 !this.state.loading &&
131 (this.state.postForm.name ||
132 this.state.postForm.url ||
133 this.state.postForm.body)
135 window.onbeforeunload = () => true;
137 window.onbeforeunload = undefined;
141 componentWillUnmount() {
142 this.subscription.unsubscribe();
143 /* this.choices && this.choices.destroy(); */
144 window.onbeforeunload = null;
152 !this.state.loading &&
153 (this.state.postForm.name ||
154 this.state.postForm.url ||
155 this.state.postForm.body)
157 message={i18n.t('block_leaving')}
159 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
160 <div class="form-group row">
161 <label class="col-sm-2 col-form-label" htmlFor="post-url">
164 <div class="col-sm-10">
169 value={this.state.postForm.url}
170 onInput={linkEvent(this, this.handlePostUrlChange)}
171 onPaste={linkEvent(this, this.handleImageUploadPaste)}
173 {this.state.suggestedTitle && (
175 class="mt-1 text-muted small font-weight-bold pointer"
176 onClick={linkEvent(this, this.copySuggestedTitle)}
178 {i18n.t('copy_suggested_title', {
179 title: this.state.suggestedTitle,
185 htmlFor="file-upload"
187 UserService.Instance.user && 'pointer'
188 } d-inline-block float-right text-muted font-weight-bold`}
189 data-tippy-content={i18n.t('upload_image')}
191 <svg class="icon icon-inline">
192 <use xlinkHref="#icon-image"></use>
198 accept="image/*,video/*"
201 disabled={!UserService.Instance.user}
202 onChange={linkEvent(this, this.handleImageUpload)}
205 {this.state.postForm.url && validURL(this.state.postForm.url) && (
207 href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
208 this.state.postForm.url
211 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
214 {i18n.t('archive_link')}
217 {this.state.imageLoading && (
218 <svg class="icon icon-spinner spin">
219 <use xlinkHref="#icon-spinner"></use>
222 {isImage(this.state.postForm.url) && (
223 <img src={this.state.postForm.url} class="img-fluid" />
225 {this.state.crossPosts.length > 0 && (
227 <div class="my-1 text-muted small font-weight-bold">
228 {i18n.t('cross_posts')}
232 posts={this.state.crossPosts}
233 enableDownvotes={this.props.enableDownvotes}
234 enableNsfw={this.props.enableNsfw}
240 <div class="form-group row">
241 <label class="col-sm-2 col-form-label" htmlFor="post-title">
244 <div class="col-sm-10">
246 value={this.state.postForm.name}
248 onInput={linkEvent(this, this.handlePostNameChange)}
249 class={`form-control ${
250 !validTitle(this.state.postForm.name) && 'is-invalid'
255 maxLength={MAX_POST_TITLE_LENGTH}
257 {!validTitle(this.state.postForm.name) && (
258 <div class="invalid-feedback">
259 {i18n.t('invalid_post_title')}
262 {this.state.suggestedPosts.length > 0 && (
264 <div class="my-1 text-muted small font-weight-bold">
265 {i18n.t('related_posts')}
268 posts={this.state.suggestedPosts}
269 enableDownvotes={this.props.enableDownvotes}
270 enableNsfw={this.props.enableNsfw}
277 <div class="form-group row">
278 <label class="col-sm-2 col-form-label" htmlFor={this.id}>
281 <div class="col-sm-10">
283 initialContent={this.state.postForm.body}
284 onContentChange={this.handlePostBodyChange}
288 {!this.props.post_view && (
289 <div class="form-group row">
290 <label class="col-sm-2 col-form-label" htmlFor="post-community">
291 {i18n.t('community')}
293 <div class="col-sm-10">
297 value={this.state.postForm.community_id}
298 onInput={linkEvent(this, this.handlePostCommunityChange)}
300 <option>{i18n.t('select_a_community')}</option>
301 {this.props.communities.map(cv => (
302 <option value={cv.community.id}>
305 : `${hostname(cv.community.actor_id)}/${
314 {this.props.enableNsfw && (
315 <div class="form-group row">
316 <div class="col-sm-10">
317 <div class="form-check">
319 class="form-check-input"
322 checked={this.state.postForm.nsfw}
323 onChange={linkEvent(this, this.handlePostNsfwChange)}
325 <label class="form-check-label" htmlFor="post-nsfw">
332 <div class="form-group row">
333 <div class="col-sm-10">
336 !this.state.postForm.community_id || this.state.loading
339 class="btn btn-secondary mr-2"
341 {this.state.loading ? (
342 <svg class="icon icon-spinner spin">
343 <use xlinkHref="#icon-spinner"></use>
345 ) : this.props.post_view ? (
346 capitalizeFirstLetter(i18n.t('save'))
348 capitalizeFirstLetter(i18n.t('create'))
351 {this.props.post_view && (
354 class="btn btn-secondary"
355 onClick={linkEvent(this, this.handleCancel)}
367 handlePostSubmit(i: PostForm, event: any) {
368 event.preventDefault();
370 // Coerce empty url string to undefined
371 if (i.state.postForm.url !== undefined && i.state.postForm.url === '') {
372 i.state.postForm.url = undefined;
375 if (i.props.post_view) {
376 let form: EditPost = {
378 edit_id: i.props.post_view.post.id,
380 WebSocketService.Instance.client.editPost(form);
382 WebSocketService.Instance.client.createPost(i.state.postForm);
384 i.state.loading = true;
388 copySuggestedTitle(i: PostForm) {
389 i.state.postForm.name = i.state.suggestedTitle.substring(
391 MAX_POST_TITLE_LENGTH
393 i.state.suggestedTitle = undefined;
397 handlePostUrlChange(i: PostForm, event: any) {
398 i.state.postForm.url = event.target.value;
404 if (validURL(this.state.postForm.url)) {
406 q: this.state.postForm.url,
407 type_: SearchType.Url,
408 sort: SortType.TopAll,
411 auth: UserService.Instance.authField(false),
414 WebSocketService.Instance.client.search(form);
416 // Fetch the page title
417 getPageTitle(this.state.postForm.url).then(d => {
418 this.state.suggestedTitle = d;
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 community_id: this.state.postForm.community_id,
441 auth: UserService.Instance.authField(false),
444 if (this.state.postForm.name !== '') {
445 WebSocketService.Instance.client.search(form);
447 this.state.suggestedPosts = [];
450 this.setState(this.state);
453 handlePostBodyChange(val: string) {
454 this.state.postForm.body = val;
455 this.setState(this.state);
458 handlePostCommunityChange(i: PostForm, event: any) {
459 i.state.postForm.community_id = Number(event.target.value);
463 handlePostNsfwChange(i: PostForm, event: any) {
464 i.state.postForm.nsfw = event.target.checked;
468 handleCancel(i: PostForm) {
472 handlePreviewToggle(i: PostForm, event: any) {
473 event.preventDefault();
474 i.state.previewMode = !i.state.previewMode;
478 handleImageUploadPaste(i: PostForm, event: any) {
479 let image = event.clipboardData.files[0];
481 i.handleImageUpload(i, image);
485 handleImageUpload(i: PostForm, event: any) {
488 event.preventDefault();
489 file = event.target.files[0];
494 const formData = new FormData();
495 formData.append('images[]', file);
497 i.state.imageLoading = true;
504 .then(res => res.json())
506 console.log('pictrs upload:');
508 if (res.msg == 'ok') {
509 let hash = res.files[0].file;
510 let url = `${pictrsUri}/${hash}`;
511 let deleteToken = res.files[0].delete_token;
512 let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
513 i.state.postForm.url = url;
514 i.state.imageLoading = false;
517 i18n.t('click_to_delete_picture'),
518 i18n.t('picture_deleted'),
522 i.state.imageLoading = false;
524 toast(JSON.stringify(res), 'danger');
528 i.state.imageLoading = false;
530 toast(error, 'danger');
535 // Set up select searching
537 let selectId: any = document.getElementById('post-community');
539 this.choices = new Choices(selectId, {
542 containerOuter: 'choices',
543 containerInner: 'choices__inner bg-light border-0',
544 input: 'form-control',
545 inputCloned: 'choices__input--cloned',
546 list: 'choices__list',
547 listItems: 'choices__list--multiple',
548 listSingle: 'choices__list--single',
549 listDropdown: 'choices__list--dropdown',
550 item: 'choices__item bg-light',
551 itemSelectable: 'choices__item--selectable',
552 itemDisabled: 'choices__item--disabled',
553 itemChoice: 'choices__item--choice',
554 placeholder: 'choices__placeholder',
555 group: 'choices__group',
556 groupHeading: 'choices__heading',
557 button: 'choices__button',
558 activeState: 'is-active',
559 focusState: 'is-focused',
560 openState: 'is-open',
561 disabledState: 'is-disabled',
562 highlightedState: 'text-info',
563 selectedState: 'text-info',
564 flippedState: 'is-flipped',
565 loadingState: 'is-loading',
566 noResults: 'has-no-results',
567 noChoices: 'has-no-choices',
570 this.choices.passedElement.element.addEventListener(
573 this.state.postForm.community_id = Number(e.detail.choice.value);
574 this.setState(this.state);
581 if (this.props.post_view) {
582 this.state.postForm.community_id = this.props.post_view.community.id;
585 (this.props.params.community_id || this.props.params.community_name)
587 if (this.props.params.community_name) {
588 let foundCommunityId = this.props.communities.find(
589 r => r.community.name == this.props.params.community_name
591 this.state.postForm.community_id = foundCommunityId;
592 } else if (this.props.params.community_id) {
593 this.state.postForm.community_id = this.props.params.community_id;
597 this.choices.setChoiceByValue(
598 this.state.postForm.community_id.toString()
601 this.setState(this.state);
603 // By default, the null valued 'Select a Community'
607 parseMessage(msg: any) {
608 let op = wsUserOp(msg);
610 toast(i18n.t(msg.error), 'danger');
611 this.state.loading = false;
612 this.setState(this.state);
614 } else if (op == UserOperation.CreatePost) {
615 let data = wsJsonToRes<PostResponse>(msg).data;
616 if (data.post_view.creator.id == UserService.Instance.user.id) {
617 this.state.loading = false;
618 this.props.onCreate(data.post_view);
620 } else if (op == UserOperation.EditPost) {
621 let data = wsJsonToRes<PostResponse>(msg).data;
622 if (data.post_view.creator.id == UserService.Instance.user.id) {
623 this.state.loading = false;
624 this.props.onEdit(data.post_view);
626 } else if (op == UserOperation.Search) {
627 let data = wsJsonToRes<SearchResponse>(msg).data;
629 if (data.type_ == SearchType[SearchType.Posts]) {
630 this.state.suggestedPosts = data.posts;
631 } else if (data.type_ == SearchType[SearchType.Url]) {
632 this.state.crossPosts = data.posts;
634 this.setState(this.state);