]> Untitled Git - lemmy.git/blob - ui/src/components/comment-form.tsx
Initial post-listing community non-local.
[lemmy.git] / ui / src / components / comment-form.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
3 import { retryWhen, delay, take } from 'rxjs/operators';
4 import { Prompt } from 'inferno-router';
5 import {
6   CommentNode as CommentNodeI,
7   CommentForm as CommentFormI,
8   WebSocketJsonResponse,
9   UserOperation,
10   CommentResponse,
11 } from '../interfaces';
12 import {
13   capitalizeFirstLetter,
14   mdToHtml,
15   randomStr,
16   markdownHelpUrl,
17   toast,
18   setupTribute,
19   wsJsonToRes,
20 } from '../utils';
21 import { WebSocketService, UserService } from '../services';
22 import autosize from 'autosize';
23 import Tribute from 'tributejs/src/Tribute.js';
24 import { i18n } from '../i18next';
25
26 interface CommentFormProps {
27   postId?: number;
28   node?: CommentNodeI;
29   onReplyCancel?(): any;
30   edit?: boolean;
31   disabled?: boolean;
32 }
33
34 interface CommentFormState {
35   commentForm: CommentFormI;
36   buttonTitle: string;
37   previewMode: boolean;
38   loading: boolean;
39   imageLoading: boolean;
40 }
41
42 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
43   private id = `comment-textarea-${randomStr()}`;
44   private formId = `comment-form-${randomStr()}`;
45   private tribute: Tribute;
46   private subscription: Subscription;
47   private emptyState: CommentFormState = {
48     commentForm: {
49       auth: null,
50       content: null,
51       post_id: this.props.node
52         ? this.props.node.comment.post_id
53         : this.props.postId,
54       creator_id: UserService.Instance.user
55         ? UserService.Instance.user.id
56         : null,
57     },
58     buttonTitle: !this.props.node
59       ? capitalizeFirstLetter(i18n.t('post'))
60       : this.props.edit
61       ? capitalizeFirstLetter(i18n.t('edit'))
62       : capitalizeFirstLetter(i18n.t('reply')),
63     previewMode: false,
64     loading: false,
65     imageLoading: false,
66   };
67
68   constructor(props: any, context: any) {
69     super(props, context);
70
71     this.tribute = setupTribute();
72     this.state = this.emptyState;
73
74     if (this.props.node) {
75       if (this.props.edit) {
76         this.state.commentForm.edit_id = this.props.node.comment.id;
77         this.state.commentForm.parent_id = this.props.node.comment.parent_id;
78         this.state.commentForm.content = this.props.node.comment.content;
79         this.state.commentForm.creator_id = this.props.node.comment.creator_id;
80       } else {
81         // A reply gets a new parent id
82         this.state.commentForm.parent_id = this.props.node.comment.id;
83       }
84     }
85
86     this.subscription = WebSocketService.Instance.subject
87       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
88       .subscribe(
89         msg => this.parseMessage(msg),
90         err => console.error(err),
91         () => console.log('complete')
92       );
93   }
94
95   componentDidMount() {
96     var textarea: any = document.getElementById(this.id);
97     autosize(textarea);
98     this.tribute.attach(textarea);
99     textarea.addEventListener('tribute-replaced', () => {
100       this.state.commentForm.content = textarea.value;
101       this.setState(this.state);
102       autosize.update(textarea);
103     });
104   }
105
106   componentWillUnmount() {
107     this.subscription.unsubscribe();
108   }
109
110   render() {
111     return (
112       <div class="mb-3">
113         <Prompt
114           when={this.state.commentForm.content}
115           message={i18n.t('block_leaving')}
116         />
117         <form
118           id={this.formId}
119           onSubmit={linkEvent(this, this.handleCommentSubmit)}
120         >
121           <div class="form-group row">
122             <div className={`col-sm-12`}>
123               <textarea
124                 id={this.id}
125                 className={`form-control ${this.state.previewMode && 'd-none'}`}
126                 value={this.state.commentForm.content}
127                 onInput={linkEvent(this, this.handleCommentContentChange)}
128                 onPaste={linkEvent(this, this.handleImageUploadPaste)}
129                 required
130                 disabled={this.props.disabled}
131                 rows={2}
132                 maxLength={10000}
133               />
134               {this.state.previewMode && (
135                 <div
136                   className="md-div"
137                   dangerouslySetInnerHTML={mdToHtml(
138                     this.state.commentForm.content
139                   )}
140                 />
141               )}
142             </div>
143           </div>
144           <div class="row">
145             <div class="col-sm-12">
146               <button
147                 type="submit"
148                 class="btn btn-sm btn-secondary mr-2"
149                 disabled={this.props.disabled}
150               >
151                 {this.state.loading ? (
152                   <svg class="icon icon-spinner spin">
153                     <use xlinkHref="#icon-spinner"></use>
154                   </svg>
155                 ) : (
156                   <span>{this.state.buttonTitle}</span>
157                 )}
158               </button>
159               {this.state.commentForm.content && (
160                 <button
161                   className={`btn btn-sm mr-2 btn-secondary ${this.state
162                     .previewMode && 'active'}`}
163                   onClick={linkEvent(this, this.handlePreviewToggle)}
164                 >
165                   {i18n.t('preview')}
166                 </button>
167               )}
168               {this.props.node && (
169                 <button
170                   type="button"
171                   class="btn btn-sm btn-secondary mr-2"
172                   onClick={linkEvent(this, this.handleReplyCancel)}
173                 >
174                   {i18n.t('cancel')}
175                 </button>
176               )}
177               <a
178                 href={markdownHelpUrl}
179                 target="_blank"
180                 class="d-inline-block float-right text-muted font-weight-bold"
181                 title={i18n.t('formatting_help')}
182               >
183                 <svg class="icon icon-inline">
184                   <use xlinkHref="#icon-help-circle"></use>
185                 </svg>
186               </a>
187               <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
188                 <label
189                   htmlFor={`file-upload-${this.id}`}
190                   className={`${UserService.Instance.user && 'pointer'}`}
191                   data-tippy-content={i18n.t('upload_image')}
192                 >
193                   <svg class="icon icon-inline">
194                     <use xlinkHref="#icon-image"></use>
195                   </svg>
196                 </label>
197                 <input
198                   id={`file-upload-${this.id}`}
199                   type="file"
200                   accept="image/*,video/*"
201                   name="file"
202                   class="d-none"
203                   disabled={!UserService.Instance.user}
204                   onChange={linkEvent(this, this.handleImageUpload)}
205                 />
206               </form>
207               {this.state.imageLoading && (
208                 <svg class="icon icon-spinner spin">
209                   <use xlinkHref="#icon-spinner"></use>
210                 </svg>
211               )}
212             </div>
213           </div>
214         </form>
215       </div>
216     );
217   }
218
219   handleFinished() {
220     this.state.previewMode = false;
221     this.state.loading = false;
222     this.state.commentForm.content = '';
223     this.setState(this.state);
224     let form: any = document.getElementById(this.formId);
225     form.reset();
226     if (this.props.node) {
227       this.props.onReplyCancel();
228     }
229     autosize.update(document.querySelector('textarea'));
230     this.setState(this.state);
231   }
232
233   handleCommentSubmit(i: CommentForm, event: any) {
234     event.preventDefault();
235     if (i.props.edit) {
236       WebSocketService.Instance.editComment(i.state.commentForm);
237     } else {
238       WebSocketService.Instance.createComment(i.state.commentForm);
239     }
240
241     i.state.loading = true;
242     i.setState(i.state);
243   }
244
245   handleCommentContentChange(i: CommentForm, event: any) {
246     i.state.commentForm.content = event.target.value;
247     i.setState(i.state);
248   }
249
250   handlePreviewToggle(i: CommentForm, event: any) {
251     event.preventDefault();
252     i.state.previewMode = !i.state.previewMode;
253     i.setState(i.state);
254   }
255
256   handleReplyCancel(i: CommentForm) {
257     i.props.onReplyCancel();
258   }
259
260   handleImageUploadPaste(i: CommentForm, event: any) {
261     let image = event.clipboardData.files[0];
262     if (image) {
263       i.handleImageUpload(i, image);
264     }
265   }
266
267   handleImageUpload(i: CommentForm, event: any) {
268     let file: any;
269     if (event.target) {
270       event.preventDefault();
271       file = event.target.files[0];
272     } else {
273       file = event;
274     }
275
276     const imageUploadUrl = `/pictshare/api/upload.php`;
277     const formData = new FormData();
278     formData.append('file', file);
279
280     i.state.imageLoading = true;
281     i.setState(i.state);
282
283     fetch(imageUploadUrl, {
284       method: 'POST',
285       body: formData,
286     })
287       .then(res => res.json())
288       .then(res => {
289         let url = `${window.location.origin}/pictshare/${res.url}`;
290         let imageMarkdown =
291           res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
292         let content = i.state.commentForm.content;
293         content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
294         i.state.commentForm.content = content;
295         i.state.imageLoading = false;
296         i.setState(i.state);
297         let textarea: any = document.getElementById(i.id);
298         autosize.update(textarea);
299       })
300       .catch(error => {
301         i.state.imageLoading = false;
302         i.setState(i.state);
303         toast(error, 'danger');
304       });
305   }
306
307   parseMessage(msg: WebSocketJsonResponse) {
308     let res = wsJsonToRes(msg);
309
310     // Only do the showing and hiding if logged in
311     if (UserService.Instance.user) {
312       if (res.op == UserOperation.CreateComment) {
313         let data = res.data as CommentResponse;
314         if (data.comment.creator_id == UserService.Instance.user.id) {
315           this.handleFinished();
316         }
317       } else if (res.op == UserOperation.EditComment) {
318         let data = res.data as CommentResponse;
319         if (data.comment.creator_id == UserService.Instance.user.id) {
320           this.handleFinished();
321         }
322       }
323     }
324   }
325 }