]> Untitled Git - lemmy.git/blob - ui/src/components/comment-form.tsx
Spanish translations
[lemmy.git] / ui / src / components / comment-form.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
3 import { Subscription } from "rxjs";
4 import { capitalizeFirstLetter, mentionDropdownFetchLimit, msgOp, mdToHtml, randomStr, markdownHelpUrl } from '../utils';
5 import { WebSocketService, UserService } from '../services';
6 import * as autosize from 'autosize';
7 import { i18n } from '../i18next';
8 import { T } from 'inferno-i18next';
9 import Tribute from 'tributejs/src/Tribute.js';
10 import * as emojiShortName from 'emoji-short-name';
11
12 interface CommentFormProps {
13   postId?: number;
14   node?: CommentNodeI;
15   onReplyCancel?(): any;
16   edit?: boolean;
17   disabled?: boolean;
18 }
19
20 interface CommentFormState {
21   commentForm: CommentFormI;
22   buttonTitle: string;
23   previewMode: boolean;
24   imageLoading: boolean;
25 }
26
27 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
28
29   private id = `comment-form-${randomStr()}`;
30   private userSub: Subscription;
31   private communitySub: Subscription;
32   private tribute: any;
33   private emptyState: CommentFormState = {
34     commentForm: {
35       auth: null,
36       content: null,
37       post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
38       creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
39     },
40     buttonTitle: !this.props.node ? capitalizeFirstLetter(i18n.t('post')) : this.props.edit ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('reply')),
41     previewMode: false,
42     imageLoading: false,
43   }
44
45   constructor(props: any, context: any) {
46     super(props, context);
47
48     this.tribute = new Tribute({
49       collection: [
50         // Emojis
51         {
52           trigger: ':',
53           menuItemTemplate: (item: any) => {
54             let emoji = `:${item.original.key}:`;
55             return `${item.original.val} ${emoji}`;
56           },
57           selectTemplate: (item: any) => {
58             return `:${item.original.key}:`;
59           },
60           values: Object.entries(emojiShortName).map(e => {return {'key': e[1], 'val': e[0]}}),
61           allowSpaces: false,
62           autocompleteMode: true,
63           menuItemLimit: mentionDropdownFetchLimit,
64         },
65         // Users
66         {
67           trigger: '@',
68           selectTemplate: (item: any) => {
69             return `[/u/${item.original.key}](/u/${item.original.key})`;
70           },
71           values: (text: string, cb: any) => {
72             this.userSearch(text, (users: any) => cb(users));
73           },
74           allowSpaces: false,
75           autocompleteMode: true,
76           menuItemLimit: mentionDropdownFetchLimit,
77         },
78
79         // Communities
80         {
81           trigger: '#',
82           selectTemplate: (item: any) => {
83             return `[/c/${item.original.key}](/c/${item.original.key})`;
84           },
85           values: (text: string, cb: any) => {
86             this.communitySearch(text, (communities: any) => cb(communities));
87           },
88           allowSpaces: false,
89           autocompleteMode: true,
90           menuItemLimit: mentionDropdownFetchLimit,
91         }
92       ]
93     });
94
95     this.state = this.emptyState;
96
97     if (this.props.node) {
98       if (this.props.edit) {
99         this.state.commentForm.edit_id = this.props.node.comment.id;
100         this.state.commentForm.parent_id = this.props.node.comment.parent_id;
101         this.state.commentForm.content = this.props.node.comment.content;
102         this.state.commentForm.creator_id = this.props.node.comment.creator_id;
103       } else {
104         // A reply gets a new parent id
105         this.state.commentForm.parent_id = this.props.node.comment.id;
106       }
107     }  
108   }
109
110   componentDidMount() {
111     var textarea: any = document.getElementById(this.id);
112     autosize(textarea);
113     this.tribute.attach(textarea);
114     textarea.addEventListener('tribute-replaced', () => {
115       this.state.commentForm.content = textarea.value;
116       this.setState(this.state);
117       autosize.update(textarea);
118     });
119   }
120
121   render() {
122     return (
123       <div class="mb-3">
124         <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
125           <div class="form-group row">
126             <div className={`col-sm-12`}>
127               <textarea id={this.id} className={`form-control ${this.state.previewMode && 'd-none'}`} value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
128               {this.state.previewMode && 
129                 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.commentForm.content)} />
130               }
131             </div>
132           </div>
133           <div class="row">
134             <div class="col-sm-12">
135               <button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
136               {this.state.commentForm.content &&
137                 <button className={`btn btn-sm mr-2 btn-secondary ${this.state.previewMode && 'active'}`} onClick={linkEvent(this, this.handlePreviewToggle)}><T i18nKey="preview">#</T></button>
138               }
139               {this.props.node && <button type="button" class="btn btn-sm btn-secondary mr-2" onClick={linkEvent(this, this.handleReplyCancel)}><T i18nKey="cancel">#</T></button>}
140               <a href={markdownHelpUrl} target="_blank" class="d-inline-block float-right text-muted small font-weight-bold"><T i18nKey="formatting_help">#</T></a>
141               <form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
142                 <label htmlFor={`file-upload-${this.id}`} class="pointer"><T i18nKey="upload_image">#</T></label>
143                 <input id={`file-upload-${this.id}`} type="file" accept="image/*,video/*" name="file" class="d-none" onChange={linkEvent(this, this.handleImageUpload)} />
144               </form>
145               {this.state.imageLoading && 
146                 <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg>
147               }
148             </div>
149           </div>
150         </form>
151       </div>
152     );
153   }
154
155   handleCommentSubmit(i: CommentForm, event: any) {
156     event.preventDefault();
157     if (i.props.edit) {
158       WebSocketService.Instance.editComment(i.state.commentForm);
159     } else {
160       WebSocketService.Instance.createComment(i.state.commentForm);
161     }
162
163     i.state.previewMode = false;
164     i.state.commentForm.content = undefined;
165     event.target.reset();
166     i.setState(i.state);
167     if (i.props.node) {
168       i.props.onReplyCancel();
169     }
170
171     autosize.update(document.querySelector('textarea'));
172   }
173
174   handleCommentContentChange(i: CommentForm, event: any) {
175     i.state.commentForm.content = event.target.value;
176     i.setState(i.state);
177   }
178
179   handlePreviewToggle(i: CommentForm, event: any) {
180     event.preventDefault();
181     i.state.previewMode = !i.state.previewMode;
182     i.setState(i.state);
183   }
184
185   handleReplyCancel(i: CommentForm) {
186     i.props.onReplyCancel();
187   }
188
189   handleImageUpload(i: CommentForm, event: any) {
190     event.preventDefault();
191     let file = event.target.files[0];
192     const imageUploadUrl = `/pictshare/api/upload.php`;
193     const formData = new FormData();
194     formData.append('file', file);
195
196     i.state.imageLoading = true;
197     i.setState(i.state);
198
199     fetch(imageUploadUrl, {
200       method: 'POST',
201       body: formData,
202     })
203     .then(res => res.json())
204     .then(res => {
205       let url = `${window.location.origin}/pictshare/${res.url}`;
206       let markdown = (res.filetype == 'mp4') ? `[vid](${url}/raw)` : `![](${url})`;
207       let content = i.state.commentForm.content;
208       content = (content) ? `${content} ${markdown}` : markdown;
209       i.state.commentForm.content = content;
210       i.state.imageLoading = false;
211       i.setState(i.state);
212     })
213     .catch((error) => {
214       i.state.imageLoading = false;
215       i.setState(i.state);
216       alert(error);
217     })
218   }
219   
220   userSearch(text: string, cb: any) {
221     if (text) {
222       let form: SearchForm = {
223         q: text,
224         type_: SearchType[SearchType.Users],
225         sort: SortType[SortType.TopAll],
226         page: 1,
227         limit: mentionDropdownFetchLimit,
228       };
229
230       WebSocketService.Instance.search(form);
231
232       this.userSub = WebSocketService.Instance.subject
233       .subscribe(
234         (msg) => {  
235           let op: UserOperation = msgOp(msg);
236           if (op == UserOperation.Search) {
237             let res: SearchResponse = msg;
238             let users = res.users.map(u => {return {key: u.name}});
239             cb(users);
240             this.userSub.unsubscribe();
241           }
242         },
243         (err) => console.error(err),
244         () => console.log('complete')
245       );
246     } else {
247       cb([]);
248     }
249   }
250
251   communitySearch(text: string, cb: any) {
252     if (text) {
253       let form: SearchForm = {
254         q: text,
255         type_: SearchType[SearchType.Communities],
256         sort: SortType[SortType.TopAll],
257         page: 1,
258         limit: mentionDropdownFetchLimit,
259       };
260
261       WebSocketService.Instance.search(form);
262
263       this.communitySub = WebSocketService.Instance.subject
264       .subscribe(
265         (msg) => {  
266           let op: UserOperation = msgOp(msg);
267           if (op == UserOperation.Search) {
268             let res: SearchResponse = msg;
269             let communities = res.communities.map(u => {return {key: u.name}});
270             cb(communities);
271             this.communitySub.unsubscribe();
272           }
273         },
274         (err) => console.error(err),
275         () => console.log('complete')
276       );
277     } else {
278       cb([]);
279     }
280   }
281 }