]> Untitled Git - lemmy.git/blob - ui/src/components/comment-form.tsx
Merge branch 'yerba_master'
[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     let form: any = document.getElementById(this.formId);
224     form.reset();
225     if (this.props.node) {
226       this.props.onReplyCancel();
227     }
228     autosize.update(document.querySelector('textarea'));
229     this.setState(this.state);
230   }
231
232   handleCommentSubmit(i: CommentForm, event: any) {
233     event.preventDefault();
234     if (i.props.edit) {
235       WebSocketService.Instance.editComment(i.state.commentForm);
236     } else {
237       WebSocketService.Instance.createComment(i.state.commentForm);
238     }
239
240     i.state.loading = true;
241     i.setState(i.state);
242   }
243
244   handleCommentContentChange(i: CommentForm, event: any) {
245     i.state.commentForm.content = event.target.value;
246     i.setState(i.state);
247   }
248
249   handlePreviewToggle(i: CommentForm, event: any) {
250     event.preventDefault();
251     i.state.previewMode = !i.state.previewMode;
252     i.setState(i.state);
253   }
254
255   handleReplyCancel(i: CommentForm) {
256     i.props.onReplyCancel();
257   }
258
259   handleImageUploadPaste(i: CommentForm, event: any) {
260     let image = event.clipboardData.files[0];
261     if (image) {
262       i.handleImageUpload(i, image);
263     }
264   }
265
266   handleImageUpload(i: CommentForm, event: any) {
267     let file: any;
268     if (event.target) {
269       event.preventDefault();
270       file = event.target.files[0];
271     } else {
272       file = event;
273     }
274
275     const imageUploadUrl = `/pictshare/api/upload.php`;
276     const formData = new FormData();
277     formData.append('file', file);
278
279     i.state.imageLoading = true;
280     i.setState(i.state);
281
282     fetch(imageUploadUrl, {
283       method: 'POST',
284       body: formData,
285     })
286       .then(res => res.json())
287       .then(res => {
288         let url = `${window.location.origin}/pictshare/${res.url}`;
289         let imageMarkdown =
290           res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
291         let content = i.state.commentForm.content;
292         content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
293         i.state.commentForm.content = content;
294         i.state.imageLoading = false;
295         i.setState(i.state);
296         let textarea: any = document.getElementById(i.id);
297         autosize.update(textarea);
298       })
299       .catch(error => {
300         i.state.imageLoading = false;
301         i.setState(i.state);
302         toast(error, 'danger');
303       });
304   }
305
306   parseMessage(msg: WebSocketJsonResponse) {
307     let res = wsJsonToRes(msg);
308
309     // Only do the showing and hiding if logged in
310     if (UserService.Instance.user) {
311       if (res.op == UserOperation.CreateComment) {
312         let data = res.data as CommentResponse;
313         if (data.comment.creator_id == UserService.Instance.user.id) {
314           this.handleFinished();
315         }
316       } else if (res.op == UserOperation.EditComment) {
317         let data = res.data as CommentResponse;
318         if (data.comment.creator_id == UserService.Instance.user.id) {
319           this.handleFinished();
320         }
321       }
322     }
323   }
324 }