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,
43 Choices = require('choices.js');
46 import { i18n } from '../i18next';
47 import { pictrsUri } from '../env';
49 const MAX_POST_TITLE_LENGTH = 200;
51 interface PostFormProps {
52 post_view?: PostView; // If a post is given, that means this is an edit
53 communities?: CommunityView[];
54 params?: PostFormParams;
56 onCreate?(post: PostView): any;
57 onEdit?(post: PostView): any;
59 enableDownvotes: boolean;
62 interface PostFormState {
65 imageLoading: boolean;
67 suggestedTitle: string;
68 suggestedPosts: PostView[];
69 crossPosts: PostView[];
72 export class PostForm extends Component<PostFormProps, PostFormState> {
73 private id = `post-form-${randomStr()}`;
74 private subscription: Subscription;
76 private emptyState: PostFormState = {
81 auth: authField(false),
86 suggestedTitle: undefined,
91 constructor(props: any, context: any) {
92 super(props, context);
93 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
94 this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
95 this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
97 this.state = this.emptyState;
100 if (this.props.post_view) {
101 this.state.postForm = {
102 body: this.props.post_view.post.body,
103 name: this.props.post_view.post.name,
104 community_id: this.props.post_view.community.id,
105 url: this.props.post_view.post.url,
106 nsfw: this.props.post_view.post.nsfw,
111 if (this.props.params) {
112 this.state.postForm.name = this.props.params.name;
113 if (this.props.params.url) {
114 this.state.postForm.url = this.props.params.url;
116 if (this.props.params.body) {
117 this.state.postForm.body = this.props.params.body;
121 this.parseMessage = this.parseMessage.bind(this);
122 this.subscription = wsSubscribe(this.parseMessage);
125 componentDidMount() {
127 this.setupCommunities();
130 componentDidUpdate() {
132 !this.state.loading &&
133 (this.state.postForm.name ||
134 this.state.postForm.url ||
135 this.state.postForm.body)
137 window.onbeforeunload = () => true;
139 window.onbeforeunload = undefined;
143 componentWillUnmount() {
144 this.subscription.unsubscribe();
145 /* this.choices && this.choices.destroy(); */
146 window.onbeforeunload = null;
154 !this.state.loading &&
155 (this.state.postForm.name ||
156 this.state.postForm.url ||
157 this.state.postForm.body)
159 message={i18n.t('block_leaving')}
161 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
162 <div class="form-group row">
163 <label class="col-sm-2 col-form-label" htmlFor="post-url">
166 <div class="col-sm-10">
171 value={this.state.postForm.url}
172 onInput={linkEvent(this, this.handlePostUrlChange)}
173 onPaste={linkEvent(this, this.handleImageUploadPaste)}
175 {this.state.suggestedTitle && (
177 class="mt-1 text-muted small font-weight-bold pointer"
178 onClick={linkEvent(this, this.copySuggestedTitle)}
180 {i18n.t('copy_suggested_title', {
181 title: this.state.suggestedTitle,
187 htmlFor="file-upload"
189 UserService.Instance.user && 'pointer'
190 } d-inline-block float-right text-muted font-weight-bold`}
191 data-tippy-content={i18n.t('upload_image')}
193 <svg class="icon icon-inline">
194 <use xlinkHref="#icon-image"></use>
200 accept="image/*,video/*"
203 disabled={!UserService.Instance.user}
204 onChange={linkEvent(this, this.handleImageUpload)}
207 {this.state.postForm.url && validURL(this.state.postForm.url) && (
209 href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
210 this.state.postForm.url
213 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
216 {i18n.t('archive_link')}
219 {this.state.imageLoading && (
220 <svg class="icon icon-spinner spin">
221 <use xlinkHref="#icon-spinner"></use>
224 {isImage(this.state.postForm.url) && (
225 <img src={this.state.postForm.url} class="img-fluid" />
227 {this.state.crossPosts.length > 0 && (
229 <div class="my-1 text-muted small font-weight-bold">
230 {i18n.t('cross_posts')}
234 posts={this.state.crossPosts}
235 enableDownvotes={this.props.enableDownvotes}
236 enableNsfw={this.props.enableNsfw}
242 <div class="form-group row">
243 <label class="col-sm-2 col-form-label" htmlFor="post-title">
246 <div class="col-sm-10">
248 value={this.state.postForm.name}
250 onInput={linkEvent(this, this.handlePostNameChange)}
251 class={`form-control ${
252 !validTitle(this.state.postForm.name) && 'is-invalid'
257 maxLength={MAX_POST_TITLE_LENGTH}
259 {!validTitle(this.state.postForm.name) && (
260 <div class="invalid-feedback">
261 {i18n.t('invalid_post_title')}
264 {this.state.suggestedPosts.length > 0 && (
266 <div class="my-1 text-muted small font-weight-bold">
267 {i18n.t('related_posts')}
270 posts={this.state.suggestedPosts}
271 enableDownvotes={this.props.enableDownvotes}
272 enableNsfw={this.props.enableNsfw}
279 <div class="form-group row">
280 <label class="col-sm-2 col-form-label" htmlFor={this.id}>
283 <div class="col-sm-10">
285 initialContent={this.state.postForm.body}
286 onContentChange={this.handlePostBodyChange}
290 {!this.props.post_view && (
291 <div class="form-group row">
292 <label class="col-sm-2 col-form-label" htmlFor="post-community">
293 {i18n.t('community')}
295 <div class="col-sm-10">
299 value={this.state.postForm.community_id}
300 onInput={linkEvent(this, this.handlePostCommunityChange)}
302 <option>{i18n.t('select_a_community')}</option>
303 {this.props.communities.map(cv => (
304 <option value={cv.community.id}>
307 : `${hostname(cv.community.actor_id)}/${
316 {this.props.enableNsfw && (
317 <div class="form-group row">
318 <div class="col-sm-10">
319 <div class="form-check">
321 class="form-check-input"
324 checked={this.state.postForm.nsfw}
325 onChange={linkEvent(this, this.handlePostNsfwChange)}
327 <label class="form-check-label" htmlFor="post-nsfw">
334 <div class="form-group row">
335 <div class="col-sm-10">
338 !this.state.postForm.community_id || this.state.loading
341 class="btn btn-secondary mr-2"
343 {this.state.loading ? (
344 <svg class="icon icon-spinner spin">
345 <use xlinkHref="#icon-spinner"></use>
347 ) : this.props.post_view ? (
348 capitalizeFirstLetter(i18n.t('save'))
350 capitalizeFirstLetter(i18n.t('create'))
353 {this.props.post_view && (
356 class="btn btn-secondary"
357 onClick={linkEvent(this, this.handleCancel)}
369 handlePostSubmit(i: PostForm, event: any) {
370 event.preventDefault();
372 // Coerce empty url string to undefined
373 if (i.state.postForm.url !== undefined && i.state.postForm.url === '') {
374 i.state.postForm.url = undefined;
377 if (i.props.post_view) {
378 let form: EditPost = {
380 post_id: i.props.post_view.post.id,
382 WebSocketService.Instance.send(wsClient.editPost(form));
384 WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
386 i.state.loading = true;
390 copySuggestedTitle(i: PostForm) {
391 i.state.postForm.name = i.state.suggestedTitle.substring(
393 MAX_POST_TITLE_LENGTH
395 i.state.suggestedTitle = undefined;
399 handlePostUrlChange(i: PostForm, event: any) {
400 i.state.postForm.url = event.target.value;
406 if (validURL(this.state.postForm.url)) {
408 q: this.state.postForm.url,
409 type_: SearchType.Url,
410 sort: SortType.TopAll,
413 auth: authField(false),
416 WebSocketService.Instance.send(wsClient.search(form));
418 // Fetch the page title
419 getPageTitle(this.state.postForm.url).then(d => {
420 this.state.suggestedTitle = d;
421 this.setState(this.state);
424 this.state.suggestedTitle = undefined;
425 this.state.crossPosts = [];
429 handlePostNameChange(i: PostForm, event: any) {
430 i.state.postForm.name = event.target.value;
432 i.fetchSimilarPosts();
435 fetchSimilarPosts() {
437 q: this.state.postForm.name,
438 type_: SearchType.Posts,
439 sort: SortType.TopAll,
440 community_id: this.state.postForm.community_id,
443 auth: authField(false),
446 if (this.state.postForm.name !== '') {
447 WebSocketService.Instance.send(wsClient.search(form));
449 this.state.suggestedPosts = [];
452 this.setState(this.state);
455 handlePostBodyChange(val: string) {
456 this.state.postForm.body = val;
457 this.setState(this.state);
460 handlePostCommunityChange(i: PostForm, event: any) {
461 i.state.postForm.community_id = Number(event.target.value);
465 handlePostNsfwChange(i: PostForm, event: any) {
466 i.state.postForm.nsfw = event.target.checked;
470 handleCancel(i: PostForm) {
474 handlePreviewToggle(i: PostForm, event: any) {
475 event.preventDefault();
476 i.state.previewMode = !i.state.previewMode;
480 handleImageUploadPaste(i: PostForm, event: any) {
481 let image = event.clipboardData.files[0];
483 i.handleImageUpload(i, image);
487 handleImageUpload(i: PostForm, event: any) {
490 event.preventDefault();
491 file = event.target.files[0];
496 const formData = new FormData();
497 formData.append('images[]', file);
499 i.state.imageLoading = true;
506 .then(res => res.json())
508 console.log('pictrs upload:');
510 if (res.msg == 'ok') {
511 let hash = res.files[0].file;
512 let url = `${pictrsUri}/${hash}`;
513 let deleteToken = res.files[0].delete_token;
514 let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
515 i.state.postForm.url = url;
516 i.state.imageLoading = false;
519 i18n.t('click_to_delete_picture'),
520 i18n.t('picture_deleted'),
524 i.state.imageLoading = false;
526 toast(JSON.stringify(res), 'danger');
530 i.state.imageLoading = false;
532 toast(error, 'danger');
537 // Set up select searching
539 let selectId: any = document.getElementById('post-community');
541 this.choices = new Choices(selectId, {
544 containerOuter: 'choices',
545 containerInner: 'choices__inner bg-light border-0',
546 input: 'form-control',
547 inputCloned: 'choices__input--cloned',
548 list: 'choices__list',
549 listItems: 'choices__list--multiple',
550 listSingle: 'choices__list--single',
551 listDropdown: 'choices__list--dropdown',
552 item: 'choices__item bg-light',
553 itemSelectable: 'choices__item--selectable',
554 itemDisabled: 'choices__item--disabled',
555 itemChoice: 'choices__item--choice',
556 placeholder: 'choices__placeholder',
557 group: 'choices__group',
558 groupHeading: 'choices__heading',
559 button: 'choices__button',
560 activeState: 'is-active',
561 focusState: 'is-focused',
562 openState: 'is-open',
563 disabledState: 'is-disabled',
564 highlightedState: 'text-info',
565 selectedState: 'text-info',
566 flippedState: 'is-flipped',
567 loadingState: 'is-loading',
568 noResults: 'has-no-results',
569 noChoices: 'has-no-choices',
572 this.choices.passedElement.element.addEventListener(
575 this.state.postForm.community_id = Number(e.detail.choice.value);
576 this.setState(this.state);
583 if (this.props.post_view) {
584 this.state.postForm.community_id = this.props.post_view.community.id;
587 (this.props.params.community_id || this.props.params.community_name)
589 if (this.props.params.community_name) {
590 let foundCommunityId = this.props.communities.find(
591 r => r.community.name == this.props.params.community_name
593 this.state.postForm.community_id = foundCommunityId;
594 } else if (this.props.params.community_id) {
595 this.state.postForm.community_id = this.props.params.community_id;
599 this.choices.setChoiceByValue(
600 this.state.postForm.community_id.toString()
603 this.setState(this.state);
605 // By default, the null valued 'Select a Community'
609 parseMessage(msg: any) {
610 let op = wsUserOp(msg);
612 toast(i18n.t(msg.error), 'danger');
613 this.state.loading = false;
614 this.setState(this.state);
616 } else if (op == UserOperation.CreatePost) {
617 let data = wsJsonToRes<PostResponse>(msg).data;
618 if (data.post_view.creator.id == UserService.Instance.user.id) {
619 this.state.loading = false;
620 this.props.onCreate(data.post_view);
622 } else if (op == UserOperation.EditPost) {
623 let data = wsJsonToRes<PostResponse>(msg).data;
624 if (data.post_view.creator.id == UserService.Instance.user.id) {
625 this.state.loading = false;
626 this.props.onEdit(data.post_view);
628 } else if (op == UserOperation.Search) {
629 let data = wsJsonToRes<SearchResponse>(msg).data;
631 if (data.type_ == SearchType[SearchType.Posts]) {
632 this.state.suggestedPosts = data.posts;
633 } else if (data.type_ == SearchType[SearchType.Url]) {
634 this.state.crossPosts = data.posts;
636 this.setState(this.state);