1 import { Component, linkEvent } from 'inferno';
2 import { PostListings } from './post-listings';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
12 ListCommunitiesResponse,
18 } from '../interfaces';
19 import { WebSocketService, UserService } from '../services';
24 capitalizeFirstLetter,
31 import * as autosize from 'autosize';
32 import { i18n } from '../i18next';
33 import { T } from 'inferno-i18next';
35 interface PostFormProps {
36 post?: Post; // If a post is given, that means this is an edit
37 params?: PostFormParams;
39 onCreate?(id: number): any;
40 onEdit?(post: Post): any;
43 interface PostFormState {
45 communities: Array<Community>;
47 imageLoading: boolean;
49 suggestedTitle: string;
50 suggestedPosts: Array<Post>;
51 crossPosts: Array<Post>;
54 export class PostForm extends Component<PostFormProps, PostFormState> {
55 private subscription: Subscription;
56 private emptyState: PostFormState = {
62 creator_id: UserService.Instance.user
63 ? UserService.Instance.user.id
70 suggestedTitle: undefined,
75 constructor(props: any, context: any) {
76 super(props, context);
77 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
78 this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
80 this.state = this.emptyState;
82 if (this.props.post) {
83 this.state.postForm = {
84 body: this.props.post.body,
85 // NOTE: debouncing breaks both these for some reason, unless you use defaultValue
86 name: this.props.post.name,
87 community_id: this.props.post.community_id,
88 edit_id: this.props.post.id,
89 creator_id: this.props.post.creator_id,
90 url: this.props.post.url,
91 nsfw: this.props.post.nsfw,
96 if (this.props.params) {
97 this.state.postForm.name = this.props.params.name;
98 if (this.props.params.url) {
99 this.state.postForm.url = this.props.params.url;
101 if (this.props.params.body) {
102 this.state.postForm.body = this.props.params.body;
106 this.subscription = WebSocketService.Instance.subject
116 msg => this.parseMessage(msg),
117 err => console.error(err),
118 () => console.log('complete')
121 let listCommunitiesForm: ListCommunitiesForm = {
122 sort: SortType[SortType.TopAll],
126 WebSocketService.Instance.listCommunities(listCommunitiesForm);
129 componentDidMount() {
130 autosize(document.querySelectorAll('textarea'));
133 componentWillUnmount() {
134 this.subscription.unsubscribe();
140 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
141 <div class="form-group row">
142 <label class="col-sm-2 col-form-label">
143 <T i18nKey="url">#</T>
145 <div class="col-sm-10">
149 value={this.state.postForm.url}
150 onInput={linkEvent(this, this.handlePostUrlChange)}
152 {this.state.suggestedTitle && (
154 class="mt-1 text-muted small font-weight-bold pointer"
155 onClick={linkEvent(this, this.copySuggestedTitle)}
158 i18nKey="copy_suggested_title"
159 interpolation={{ title: this.state.suggestedTitle }}
167 htmlFor="file-upload"
168 className={`${UserService.Instance.user &&
169 'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}
171 <T i18nKey="upload_image">#</T>
176 accept="image/*,video/*"
179 disabled={!UserService.Instance.user}
180 onChange={linkEvent(this, this.handleImageUpload)}
183 {validURL(this.state.postForm.url) && (
185 href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
186 this.state.postForm.url
189 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
191 <T i18nKey="archive_link">#</T>
194 {this.state.imageLoading && (
195 <svg class="icon icon-spinner spin">
196 <use xlinkHref="#icon-spinner"></use>
199 {isImage(this.state.postForm.url) && (
200 <img src={this.state.postForm.url} class="img-fluid" />
202 {this.state.crossPosts.length > 0 && (
204 <div class="my-1 text-muted small font-weight-bold">
205 <T i18nKey="cross_posts">#</T>
207 <PostListings showCommunity posts={this.state.crossPosts} />
212 <div class="form-group row">
213 <label class="col-sm-2 col-form-label">
214 <T i18nKey="title">#</T>
216 <div class="col-sm-10">
218 value={this.state.postForm.name}
219 onInput={linkEvent(this, this.handlePostNameChange)}
226 {this.state.suggestedPosts.length > 0 && (
228 <div class="my-1 text-muted small font-weight-bold">
229 <T i18nKey="related_posts">#</T>
231 <PostListings posts={this.state.suggestedPosts} />
236 <div class="form-group row">
237 <label class="col-sm-2 col-form-label">
238 <T i18nKey="body">#</T>
240 <div class="col-sm-10">
242 value={this.state.postForm.body}
243 onInput={linkEvent(this, this.handlePostBodyChange)}
244 className={`form-control ${this.state.previewMode && 'd-none'}`}
248 {this.state.previewMode && (
251 dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
254 {this.state.postForm.body && (
256 className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
257 .previewMode && 'active'}`}
258 onClick={linkEvent(this, this.handlePreviewToggle)}
260 <T i18nKey="preview">#</T>
264 href={markdownHelpUrl}
266 class="d-inline-block float-right text-muted small font-weight-bold"
268 <T i18nKey="formatting_help">#</T>
272 {!this.props.post && (
273 <div class="form-group row">
274 <label class="col-sm-2 col-form-label">
275 <T i18nKey="community">#</T>
277 <div class="col-sm-10">
280 value={this.state.postForm.community_id}
281 onInput={linkEvent(this, this.handlePostCommunityChange)}
283 {this.state.communities.map(community => (
284 <option value={community.id}>{community.name}</option>
290 {WebSocketService.Instance.site.enable_nsfw && (
291 <div class="form-group row">
292 <div class="col-sm-10">
293 <div class="form-check">
295 class="form-check-input"
297 checked={this.state.postForm.nsfw}
298 onChange={linkEvent(this, this.handlePostNsfwChange)}
300 <label class="form-check-label">
301 <T i18nKey="nsfw">#</T>
307 <div class="form-group row">
308 <div class="col-sm-10">
309 <button type="submit" class="btn btn-secondary mr-2">
310 {this.state.loading ? (
311 <svg class="icon icon-spinner spin">
312 <use xlinkHref="#icon-spinner"></use>
314 ) : this.props.post ? (
315 capitalizeFirstLetter(i18n.t('save'))
317 capitalizeFirstLetter(i18n.t('create'))
320 {this.props.post && (
323 class="btn btn-secondary"
324 onClick={linkEvent(this, this.handleCancel)}
326 <T i18nKey="cancel">#</T>
336 handlePostSubmit(i: PostForm, event: any) {
337 event.preventDefault();
339 WebSocketService.Instance.editPost(i.state.postForm);
341 WebSocketService.Instance.createPost(i.state.postForm);
343 i.state.loading = true;
347 copySuggestedTitle(i: PostForm) {
348 i.state.postForm.name = i.state.suggestedTitle;
349 i.state.suggestedTitle = undefined;
353 handlePostUrlChange(i: PostForm, event: any) {
354 i.state.postForm.url = event.target.value;
360 if (validURL(this.state.postForm.url)) {
361 let form: SearchForm = {
362 q: this.state.postForm.url,
363 type_: SearchType[SearchType.Url],
364 sort: SortType[SortType.TopAll],
369 WebSocketService.Instance.search(form);
371 // Fetch the page title
372 getPageTitle(this.state.postForm.url).then(d => {
373 this.state.suggestedTitle = d;
374 this.setState(this.state);
377 this.state.suggestedTitle = undefined;
378 this.state.crossPosts = [];
382 handlePostNameChange(i: PostForm, event: any) {
383 i.state.postForm.name = event.target.value;
385 i.fetchSimilarPosts();
388 fetchSimilarPosts() {
389 let form: SearchForm = {
390 q: this.state.postForm.name,
391 type_: SearchType[SearchType.Posts],
392 sort: SortType[SortType.TopAll],
393 community_id: this.state.postForm.community_id,
398 if (this.state.postForm.name !== '') {
399 WebSocketService.Instance.search(form);
401 this.state.suggestedPosts = [];
404 this.setState(this.state);
407 handlePostBodyChange(i: PostForm, event: any) {
408 i.state.postForm.body = event.target.value;
412 handlePostCommunityChange(i: PostForm, event: any) {
413 i.state.postForm.community_id = Number(event.target.value);
417 handlePostNsfwChange(i: PostForm, event: any) {
418 i.state.postForm.nsfw = event.target.checked;
422 handleCancel(i: PostForm) {
426 handlePreviewToggle(i: PostForm, event: any) {
427 event.preventDefault();
428 i.state.previewMode = !i.state.previewMode;
432 handleImageUpload(i: PostForm, event: any) {
433 event.preventDefault();
434 let file = event.target.files[0];
435 const imageUploadUrl = `/pictshare/api/upload.php`;
436 const formData = new FormData();
437 formData.append('file', file);
439 i.state.imageLoading = true;
442 fetch(imageUploadUrl, {
446 .then(res => res.json())
448 let url = `${window.location.origin}/pictshare/${res.url}`;
449 if (res.filetype == 'mp4') {
452 i.state.postForm.url = url;
453 i.state.imageLoading = false;
457 i.state.imageLoading = false;
463 parseMessage(msg: any) {
464 let op: UserOperation = msgOp(msg);
466 alert(i18n.t(msg.error));
467 this.state.loading = false;
468 this.setState(this.state);
470 } else if (op == UserOperation.ListCommunities) {
471 let res: ListCommunitiesResponse = msg;
472 this.state.communities = res.communities;
473 if (this.props.post) {
474 this.state.postForm.community_id = this.props.post.community_id;
475 } else if (this.props.params && this.props.params.community) {
476 let foundCommunityId = res.communities.find(
477 r => r.name == this.props.params.community
479 this.state.postForm.community_id = foundCommunityId;
481 this.state.postForm.community_id = res.communities[0].id;
483 this.setState(this.state);
484 } else if (op == UserOperation.CreatePost) {
485 this.state.loading = false;
486 let res: PostResponse = msg;
487 this.props.onCreate(res.post.id);
488 } else if (op == UserOperation.EditPost) {
489 this.state.loading = false;
490 let res: PostResponse = msg;
491 this.props.onEdit(res.post);
492 } else if (op == UserOperation.Search) {
493 let res: SearchResponse = msg;
495 if (res.type_ == SearchType[SearchType.Posts]) {
496 this.state.suggestedPosts = res.posts;
497 } else if (res.type_ == SearchType[SearchType.Url]) {
498 this.state.crossPosts = res.posts;
500 this.setState(this.state);