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 <div class="form-group row">
284 <div class="col-sm-10">
285 <div class="form-check">
287 class="form-check-input"
289 checked={this.state.postForm.nsfw}
290 onChange={linkEvent(this, this.handlePostNsfwChange)}
292 <label class="form-check-label">
293 <T i18nKey="nsfw">#</T>
298 <div class="form-group row">
299 <div class="col-sm-10">
300 <button type="submit" class="btn btn-secondary mr-2">
301 {this.state.loading ? (
302 <svg class="icon icon-spinner spin">
303 <use xlinkHref="#icon-spinner"></use>
305 ) : this.props.post ? (
306 capitalizeFirstLetter(i18n.t('save'))
308 capitalizeFirstLetter(i18n.t('create'))
311 {this.props.post && (
314 class="btn btn-secondary"
315 onClick={linkEvent(this, this.handleCancel)}
317 <T i18nKey="cancel">#</T>
327 handlePostSubmit(i: PostForm, event: any) {
328 event.preventDefault();
330 WebSocketService.Instance.editPost(i.state.postForm);
332 WebSocketService.Instance.createPost(i.state.postForm);
334 i.state.loading = true;
338 copySuggestedTitle(i: PostForm) {
339 i.state.postForm.name = i.state.suggestedTitle;
340 i.state.suggestedTitle = undefined;
344 handlePostUrlChange(i: PostForm, event: any) {
345 i.state.postForm.url = event.target.value;
346 if (validURL(i.state.postForm.url)) {
347 let form: SearchForm = {
348 q: i.state.postForm.url,
349 type_: SearchType[SearchType.Url],
350 sort: SortType[SortType.TopAll],
355 WebSocketService.Instance.search(form);
357 // Fetch the page title
358 getPageTitle(i.state.postForm.url).then(d => {
359 i.state.suggestedTitle = d;
363 i.state.suggestedTitle = undefined;
364 i.state.crossPosts = [];
370 handlePostNameChange(i: PostForm, event: any) {
371 i.state.postForm.name = event.target.value;
372 let form: SearchForm = {
373 q: i.state.postForm.name,
374 type_: SearchType[SearchType.Posts],
375 sort: SortType[SortType.TopAll],
376 community_id: i.state.postForm.community_id,
381 if (i.state.postForm.name !== '') {
382 WebSocketService.Instance.search(form);
384 i.state.suggestedPosts = [];
390 handlePostBodyChange(i: PostForm, event: any) {
391 i.state.postForm.body = event.target.value;
395 handlePostCommunityChange(i: PostForm, event: any) {
396 i.state.postForm.community_id = Number(event.target.value);
400 handlePostNsfwChange(i: PostForm, event: any) {
401 i.state.postForm.nsfw = event.target.checked;
405 handleCancel(i: PostForm) {
409 handlePreviewToggle(i: PostForm, event: any) {
410 event.preventDefault();
411 i.state.previewMode = !i.state.previewMode;
415 handleImageUpload(i: PostForm, event: any) {
416 event.preventDefault();
417 let file = event.target.files[0];
418 const imageUploadUrl = `/pictshare/api/upload.php`;
419 const formData = new FormData();
420 formData.append('file', file);
422 i.state.imageLoading = true;
425 fetch(imageUploadUrl, {
429 .then(res => res.json())
431 let url = `${window.location.origin}/pictshare/${res.url}`;
432 if (res.filetype == 'mp4') {
435 i.state.postForm.url = url;
436 i.state.imageLoading = false;
440 i.state.imageLoading = false;
446 parseMessage(msg: any) {
447 let op: UserOperation = msgOp(msg);
449 alert(i18n.t(msg.error));
450 this.state.loading = false;
451 this.setState(this.state);
453 } else if (op == UserOperation.ListCommunities) {
454 let res: ListCommunitiesResponse = msg;
455 this.state.communities = res.communities;
456 if (this.props.post) {
457 this.state.postForm.community_id = this.props.post.community_id;
458 } else if (this.props.params && this.props.params.community) {
459 let foundCommunityId = res.communities.find(
460 r => r.name == this.props.params.community
462 this.state.postForm.community_id = foundCommunityId;
464 this.state.postForm.community_id = res.communities[0].id;
466 this.setState(this.state);
467 } else if (op == UserOperation.CreatePost) {
468 this.state.loading = false;
469 let res: PostResponse = msg;
470 this.props.onCreate(res.post.id);
471 } else if (op == UserOperation.EditPost) {
472 this.state.loading = false;
473 let res: PostResponse = msg;
474 this.props.onEdit(res.post);
475 } else if (op == UserOperation.Search) {
476 let res: SearchResponse = msg;
478 if (res.type_ == SearchType[SearchType.Posts]) {
479 this.state.suggestedPosts = res.posts;
480 } else if (res.type_ == SearchType[SearchType.Url]) {
481 this.state.crossPosts = res.posts;
483 this.setState(this.state);