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';
25 capitalizeFirstLetter,
30 import * as autosize from 'autosize';
31 import { i18n } from '../i18next';
32 import { T } from 'inferno-i18next';
34 interface PostFormProps {
35 post?: Post; // If a post is given, that means this is an edit
36 params?: PostFormParams;
38 onCreate?(id: number): any;
39 onEdit?(post: Post): any;
42 interface PostFormState {
44 communities: Array<Community>;
46 imageLoading: boolean;
48 suggestedTitle: string;
49 suggestedPosts: Array<Post>;
50 crossPosts: Array<Post>;
53 export class PostForm extends Component<PostFormProps, PostFormState> {
54 private subscription: Subscription;
55 private emptyState: PostFormState = {
61 creator_id: UserService.Instance.user
62 ? UserService.Instance.user.id
69 suggestedTitle: undefined,
74 constructor(props: any, context: any) {
75 super(props, context);
77 this.state = this.emptyState;
79 if (this.props.post) {
80 this.state.postForm = {
81 body: this.props.post.body,
82 name: this.props.post.name,
83 community_id: this.props.post.community_id,
84 edit_id: this.props.post.id,
85 creator_id: this.props.post.creator_id,
86 url: this.props.post.url,
87 nsfw: this.props.post.nsfw,
92 if (this.props.params) {
93 this.state.postForm.name = this.props.params.name;
94 if (this.props.params.url) {
95 this.state.postForm.url = this.props.params.url;
97 if (this.props.params.body) {
98 this.state.postForm.body = this.props.params.body;
102 this.subscription = WebSocketService.Instance.subject
112 msg => this.parseMessage(msg),
113 err => console.error(err),
114 () => console.log('complete')
117 let listCommunitiesForm: ListCommunitiesForm = {
118 sort: SortType[SortType.TopAll],
122 WebSocketService.Instance.listCommunities(listCommunitiesForm);
125 componentDidMount() {
126 autosize(document.querySelectorAll('textarea'));
129 componentWillUnmount() {
130 this.subscription.unsubscribe();
136 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
137 <div class="form-group row">
138 <label class="col-sm-2 col-form-label">
139 <T i18nKey="url">#</T>
141 <div class="col-sm-10">
145 value={this.state.postForm.url}
146 onInput={linkEvent(this, this.handlePostUrlChange)}
148 {this.state.suggestedTitle && (
150 class="mt-1 text-muted small font-weight-bold pointer"
151 onClick={linkEvent(this, this.copySuggestedTitle)}
154 i18nKey="copy_suggested_title"
155 interpolation={{ title: this.state.suggestedTitle }}
163 htmlFor="file-upload"
164 className={`${UserService.Instance.user &&
165 'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}
167 <T i18nKey="upload_image">#</T>
172 accept="image/*,video/*"
175 disabled={!UserService.Instance.user}
176 onChange={linkEvent(this, this.handleImageUpload)}
179 {validURL(this.state.postForm.url) && (
181 href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
182 this.state.postForm.url
185 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
187 <T i18nKey="archive_link">#</T>
190 {this.state.imageLoading && (
191 <svg class="icon icon-spinner spin">
192 <use xlinkHref="#icon-spinner"></use>
195 {this.state.crossPosts.length > 0 && (
197 <div class="my-1 text-muted small font-weight-bold">
198 <T i18nKey="cross_posts">#</T>
200 <PostListings showCommunity posts={this.state.crossPosts} />
205 <div class="form-group row">
206 <label class="col-sm-2 col-form-label">
207 <T i18nKey="title">#</T>
209 <div class="col-sm-10">
211 value={this.state.postForm.name}
212 onInput={linkEvent(this, this.handlePostNameChange)}
219 {this.state.suggestedPosts.length > 0 && (
221 <div class="my-1 text-muted small font-weight-bold">
222 <T i18nKey="related_posts">#</T>
224 <PostListings posts={this.state.suggestedPosts} />
229 <div class="form-group row">
230 <label class="col-sm-2 col-form-label">
231 <T i18nKey="body">#</T>
233 <div class="col-sm-10">
235 value={this.state.postForm.body}
236 onInput={linkEvent(this, this.handlePostBodyChange)}
237 className={`form-control ${this.state.previewMode && 'd-none'}`}
241 {this.state.previewMode && (
244 dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
247 {this.state.postForm.body && (
249 className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
250 .previewMode && 'active'}`}
251 onClick={linkEvent(this, this.handlePreviewToggle)}
253 <T i18nKey="preview">#</T>
257 href={markdownHelpUrl}
259 class="d-inline-block float-right text-muted small font-weight-bold"
261 <T i18nKey="formatting_help">#</T>
265 {!this.props.post && (
266 <div class="form-group row">
267 <label class="col-sm-2 col-form-label">
268 <T i18nKey="community">#</T>
270 <div class="col-sm-10">
273 value={this.state.postForm.community_id}
274 onInput={linkEvent(this, this.handlePostCommunityChange)}
276 {this.state.communities.map(community => (
277 <option value={community.id}>{community.name}</option>
283 {WebSocketService.Instance.site.enable_nsfw && (
284 <div class="form-group row">
285 <div class="col-sm-10">
286 <div class="form-check">
288 class="form-check-input"
290 checked={this.state.postForm.nsfw}
291 onChange={linkEvent(this, this.handlePostNsfwChange)}
293 <label class="form-check-label">
294 <T i18nKey="nsfw">#</T>
300 <div class="form-group row">
301 <div class="col-sm-10">
302 <button type="submit" class="btn btn-secondary mr-2">
303 {this.state.loading ? (
304 <svg class="icon icon-spinner spin">
305 <use xlinkHref="#icon-spinner"></use>
307 ) : this.props.post ? (
308 capitalizeFirstLetter(i18n.t('save'))
310 capitalizeFirstLetter(i18n.t('create'))
313 {this.props.post && (
316 class="btn btn-secondary"
317 onClick={linkEvent(this, this.handleCancel)}
319 <T i18nKey="cancel">#</T>
329 handlePostSubmit(i: PostForm, event: any) {
330 event.preventDefault();
332 WebSocketService.Instance.editPost(i.state.postForm);
334 WebSocketService.Instance.createPost(i.state.postForm);
336 i.state.loading = true;
340 copySuggestedTitle(i: PostForm) {
341 i.state.postForm.name = i.state.suggestedTitle;
342 i.state.suggestedTitle = undefined;
346 handlePostUrlChange = debounce((i: PostForm, event: any) => {
347 i.state.postForm.url = event.target.value;
348 if (validURL(i.state.postForm.url)) {
349 let form: SearchForm = {
350 q: i.state.postForm.url,
351 type_: SearchType[SearchType.Url],
352 sort: SortType[SortType.TopAll],
357 WebSocketService.Instance.search(form);
359 // Fetch the page title
360 getPageTitle(i.state.postForm.url).then(d => {
361 i.state.suggestedTitle = d;
365 i.state.suggestedTitle = undefined;
366 i.state.crossPosts = [];
372 handlePostNameChange = debounce((i: PostForm, event: any) => {
373 i.state.postForm.name = event.target.value;
374 let form: SearchForm = {
375 q: i.state.postForm.name,
376 type_: SearchType[SearchType.Posts],
377 sort: SortType[SortType.TopAll],
378 community_id: i.state.postForm.community_id,
383 if (i.state.postForm.name !== '') {
384 WebSocketService.Instance.search(form);
386 i.state.suggestedPosts = [];
392 handlePostBodyChange(i: PostForm, event: any) {
393 i.state.postForm.body = event.target.value;
397 handlePostCommunityChange(i: PostForm, event: any) {
398 i.state.postForm.community_id = Number(event.target.value);
402 handlePostNsfwChange(i: PostForm, event: any) {
403 i.state.postForm.nsfw = event.target.checked;
407 handleCancel(i: PostForm) {
411 handlePreviewToggle(i: PostForm, event: any) {
412 event.preventDefault();
413 i.state.previewMode = !i.state.previewMode;
417 handleImageUpload(i: PostForm, event: any) {
418 event.preventDefault();
419 let file = event.target.files[0];
420 const imageUploadUrl = `/pictshare/api/upload.php`;
421 const formData = new FormData();
422 formData.append('file', file);
424 i.state.imageLoading = true;
427 fetch(imageUploadUrl, {
431 .then(res => res.json())
433 let url = `${window.location.origin}/pictshare/${res.url}`;
434 if (res.filetype == 'mp4') {
437 i.state.postForm.url = url;
438 i.state.imageLoading = false;
442 i.state.imageLoading = false;
448 parseMessage(msg: any) {
449 let op: UserOperation = msgOp(msg);
451 alert(i18n.t(msg.error));
452 this.state.loading = false;
453 this.setState(this.state);
455 } else if (op == UserOperation.ListCommunities) {
456 let res: ListCommunitiesResponse = msg;
457 this.state.communities = res.communities;
458 if (this.props.post) {
459 this.state.postForm.community_id = this.props.post.community_id;
460 } else if (this.props.params && this.props.params.community) {
461 let foundCommunityId = res.communities.find(
462 r => r.name == this.props.params.community
464 this.state.postForm.community_id = foundCommunityId;
466 this.state.postForm.community_id = res.communities[0].id;
468 this.setState(this.state);
469 } else if (op == UserOperation.CreatePost) {
470 this.state.loading = false;
471 let res: PostResponse = msg;
472 this.props.onCreate(res.post.id);
473 } else if (op == UserOperation.EditPost) {
474 this.state.loading = false;
475 let res: PostResponse = msg;
476 this.props.onEdit(res.post);
477 } else if (op == UserOperation.Search) {
478 let res: SearchResponse = msg;
480 if (res.type_ == SearchType[SearchType.Posts]) {
481 this.state.suggestedPosts = res.posts;
482 } else if (res.type_ == SearchType[SearchType.Url]) {
483 this.state.crossPosts = res.posts;
485 this.setState(this.state);