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,
19 WebSocketJsonResponse,
20 } from '../interfaces';
21 import { WebSocketService, UserService } from '../services';
26 capitalizeFirstLetter,
36 import autosize from 'autosize';
37 import Tribute from 'tributejs/src/Tribute.js';
38 import Selectr from 'mobius1-selectr';
39 import { i18n } from '../i18next';
41 const MAX_POST_TITLE_LENGTH = 200;
43 interface PostFormProps {
44 post?: Post; // If a post is given, that means this is an edit
45 params?: PostFormParams;
47 onCreate?(id: number): any;
48 onEdit?(post: Post): any;
51 interface PostFormState {
53 communities: Array<Community>;
55 imageLoading: boolean;
57 suggestedTitle: string;
58 suggestedPosts: Array<Post>;
59 crossPosts: Array<Post>;
63 export class PostForm extends Component<PostFormProps, PostFormState> {
64 private id = `post-form-${randomStr()}`;
65 private tribute: Tribute;
66 private subscription: Subscription;
67 private emptyState: PostFormState = {
73 creator_id: UserService.Instance.user
74 ? UserService.Instance.user.id
81 suggestedTitle: undefined,
84 enable_nsfw: undefined,
87 constructor(props: any, context: any) {
88 super(props, context);
89 this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
90 this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
92 this.tribute = setupTribute();
93 this.state = this.emptyState;
95 if (this.props.post) {
96 this.state.postForm = {
97 body: this.props.post.body,
98 // NOTE: debouncing breaks both these for some reason, unless you use defaultValue
99 name: this.props.post.name,
100 community_id: this.props.post.community_id,
101 edit_id: this.props.post.id,
102 creator_id: this.props.post.creator_id,
103 url: this.props.post.url,
104 nsfw: this.props.post.nsfw,
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.subscription = WebSocketService.Instance.subject
120 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
122 msg => this.parseMessage(msg),
123 err => console.error(err),
124 () => console.log('complete')
127 let listCommunitiesForm: ListCommunitiesForm = {
128 sort: SortType[SortType.TopAll],
132 WebSocketService.Instance.listCommunities(listCommunitiesForm);
133 WebSocketService.Instance.getSite();
136 componentDidMount() {
137 var textarea: any = document.getElementById(this.id);
139 this.tribute.attach(textarea);
140 textarea.addEventListener('tribute-replaced', () => {
141 this.state.postForm.body = textarea.value;
142 this.setState(this.state);
143 autosize.update(textarea);
147 componentWillUnmount() {
148 this.subscription.unsubscribe();
154 <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
155 <div class="form-group row">
156 <label class="col-sm-2 col-form-label" htmlFor="post-url">
159 <div class="col-sm-10">
164 value={this.state.postForm.url}
165 onInput={linkEvent(this, this.handlePostUrlChange)}
166 onPaste={linkEvent(this, this.handleImageUploadPaste)}
168 {this.state.suggestedTitle && (
170 class="mt-1 text-muted small font-weight-bold pointer"
171 onClick={linkEvent(this, this.copySuggestedTitle)}
173 {i18n.t('copy_suggested_title', {
174 title: this.state.suggestedTitle,
180 htmlFor="file-upload"
181 className={`${UserService.Instance.user &&
182 'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}
184 {i18n.t('upload_image')}
189 accept="image/*,video/*"
192 disabled={!UserService.Instance.user}
193 onChange={linkEvent(this, this.handleImageUpload)}
196 {validURL(this.state.postForm.url) && (
198 href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
199 this.state.postForm.url
202 class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
204 {i18n.t('archive_link')}
207 {this.state.imageLoading && (
208 <svg class="icon icon-spinner spin">
209 <use xlinkHref="#icon-spinner"></use>
212 {isImage(this.state.postForm.url) && (
213 <img src={this.state.postForm.url} class="img-fluid" />
215 {this.state.crossPosts.length > 0 && (
217 <div class="my-1 text-muted small font-weight-bold">
218 {i18n.t('cross_posts')}
220 <PostListings showCommunity posts={this.state.crossPosts} />
225 <div class="form-group row">
226 <label class="col-sm-2 col-form-label" htmlFor="post-title">
229 <div class="col-sm-10">
231 value={this.state.postForm.name}
233 onInput={linkEvent(this, this.handlePostNameChange)}
238 maxLength={MAX_POST_TITLE_LENGTH}
240 {this.state.suggestedPosts.length > 0 && (
242 <div class="my-1 text-muted small font-weight-bold">
243 {i18n.t('related_posts')}
245 <PostListings posts={this.state.suggestedPosts} />
251 <div class="form-group row">
252 <label class="col-sm-2 col-form-label" htmlFor={this.id}>
255 <div class="col-sm-10">
258 value={this.state.postForm.body}
259 onInput={linkEvent(this, this.handlePostBodyChange)}
260 className={`form-control ${this.state.previewMode && 'd-none'}`}
264 {this.state.previewMode && (
267 dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
270 {this.state.postForm.body && (
272 className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
273 .previewMode && 'active'}`}
274 onClick={linkEvent(this, this.handlePreviewToggle)}
280 href={markdownHelpUrl}
282 class="d-inline-block float-right text-muted small font-weight-bold"
284 {i18n.t('formatting_help')}
288 {!this.props.post && (
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 {this.state.communities.map(community => (
301 <option value={community.id}>{community.name}</option>
307 {this.state.enable_nsfw && (
308 <div class="form-group row">
309 <div class="col-sm-10">
310 <div class="form-check">
312 class="form-check-input"
315 checked={this.state.postForm.nsfw}
316 onChange={linkEvent(this, this.handlePostNsfwChange)}
318 <label class="form-check-label" htmlFor="post-nsfw">
325 <div class="form-group row">
326 <div class="col-sm-10">
327 <button type="submit" class="btn btn-secondary mr-2">
328 {this.state.loading ? (
329 <svg class="icon icon-spinner spin">
330 <use xlinkHref="#icon-spinner"></use>
332 ) : this.props.post ? (
333 capitalizeFirstLetter(i18n.t('save'))
335 capitalizeFirstLetter(i18n.t('create'))
338 {this.props.post && (
341 class="btn btn-secondary"
342 onClick={linkEvent(this, this.handleCancel)}
354 handlePostSubmit(i: PostForm, event: any) {
355 event.preventDefault();
357 WebSocketService.Instance.editPost(i.state.postForm);
359 WebSocketService.Instance.createPost(i.state.postForm);
361 i.state.loading = true;
365 copySuggestedTitle(i: PostForm) {
366 i.state.postForm.name = i.state.suggestedTitle.substring(
368 MAX_POST_TITLE_LENGTH
370 i.state.suggestedTitle = undefined;
374 handlePostUrlChange(i: PostForm, event: any) {
375 i.state.postForm.url = event.target.value;
381 if (validURL(this.state.postForm.url)) {
382 let form: SearchForm = {
383 q: this.state.postForm.url,
384 type_: SearchType[SearchType.Url],
385 sort: SortType[SortType.TopAll],
390 WebSocketService.Instance.search(form);
392 // Fetch the page title
393 getPageTitle(this.state.postForm.url).then(d => {
394 this.state.suggestedTitle = d;
395 this.setState(this.state);
398 this.state.suggestedTitle = undefined;
399 this.state.crossPosts = [];
403 handlePostNameChange(i: PostForm, event: any) {
404 i.state.postForm.name = event.target.value;
406 i.fetchSimilarPosts();
409 fetchSimilarPosts() {
410 let form: SearchForm = {
411 q: this.state.postForm.name,
412 type_: SearchType[SearchType.Posts],
413 sort: SortType[SortType.TopAll],
414 community_id: this.state.postForm.community_id,
419 if (this.state.postForm.name !== '') {
420 WebSocketService.Instance.search(form);
422 this.state.suggestedPosts = [];
425 this.setState(this.state);
428 handlePostBodyChange(i: PostForm, event: any) {
429 i.state.postForm.body = event.target.value;
433 handlePostCommunityChange(i: PostForm, event: any) {
434 i.state.postForm.community_id = Number(event.target.value);
438 handlePostNsfwChange(i: PostForm, event: any) {
439 i.state.postForm.nsfw = event.target.checked;
443 handleCancel(i: PostForm) {
447 handlePreviewToggle(i: PostForm, event: any) {
448 event.preventDefault();
449 i.state.previewMode = !i.state.previewMode;
453 handleImageUploadPaste(i: PostForm, event: any) {
454 let image = event.clipboardData.files[0];
456 i.handleImageUpload(i, image);
460 handleImageUpload(i: PostForm, event: any) {
463 event.preventDefault();
464 file = event.target.files[0];
469 const imageUploadUrl = `/pictshare/api/upload.php`;
470 const formData = new FormData();
471 formData.append('file', file);
473 i.state.imageLoading = true;
476 fetch(imageUploadUrl, {
480 .then(res => res.json())
482 let url = `${window.location.origin}/pictshare/${res.url}`;
483 if (res.filetype == 'mp4') {
486 i.state.postForm.url = url;
487 i.state.imageLoading = false;
491 i.state.imageLoading = false;
493 toast(error, 'danger');
497 parseMessage(msg: WebSocketJsonResponse) {
498 let res = wsJsonToRes(msg);
500 toast(i18n.t(msg.error), 'danger');
501 this.state.loading = false;
502 this.setState(this.state);
504 } else if (res.op == UserOperation.ListCommunities) {
505 let data = res.data as ListCommunitiesResponse;
506 this.state.communities = data.communities;
507 if (this.props.post) {
508 this.state.postForm.community_id = this.props.post.community_id;
509 } else if (this.props.params && this.props.params.community) {
510 let foundCommunityId = data.communities.find(
511 r => r.name == this.props.params.community
513 this.state.postForm.community_id = foundCommunityId;
515 this.state.postForm.community_id = data.communities[0].id;
517 this.setState(this.state);
519 // Set up select searching
520 let selectId: any = document.getElementById('post-community');
521 let selector = new Selectr(selectId, {});
522 selector.on('selectr.select', option => {
523 this.state.postForm.community_id = Number(option.value);
525 } else if (res.op == UserOperation.CreatePost) {
526 let data = res.data as PostResponse;
527 if (data.post.creator_id == UserService.Instance.user.id) {
528 this.state.loading = false;
529 this.props.onCreate(data.post.id);
531 } else if (res.op == UserOperation.EditPost) {
532 let data = res.data as PostResponse;
533 if (data.post.creator_id == UserService.Instance.user.id) {
534 this.state.loading = false;
535 this.props.onEdit(data.post);
537 } else if (res.op == UserOperation.Search) {
538 let data = res.data as SearchResponse;
540 if (data.type_ == SearchType[SearchType.Posts]) {
541 this.state.suggestedPosts = data.posts;
542 } else if (data.type_ == SearchType[SearchType.Url]) {
543 this.state.crossPosts = data.posts;
545 this.setState(this.state);
546 } else if (res.op == UserOperation.GetSite) {
547 let data = res.data as GetSiteResponse;
548 this.state.enable_nsfw = data.site.enable_nsfw;
549 this.setState(this.state);