]> Untitled Git - lemmy.git/blob - ui/src/components/comment-node.tsx
Adding emoji support
[lemmy.git] / ui / src / components / comment-node.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm } from '../interfaces';
4 import { WebSocketService, UserService } from '../services';
5 import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
6 import * as moment from 'moment';
7 import { MomentTime } from './moment-time';
8 import { CommentForm } from './comment-form';
9 import { CommentNodes } from './comment-nodes';
10
11 enum BanType {Community, Site};
12
13 interface CommentNodeState {
14   showReply: boolean;
15   showEdit: boolean;
16   showRemoveDialog: boolean;
17   removeReason: string;
18   showBanDialog: boolean;
19   banReason: string;
20   banExpires: string;
21   banType: BanType;
22 }
23
24 interface CommentNodeProps {
25   node: CommentNodeI;
26   noIndent?: boolean;
27   viewOnly?: boolean;
28   locked?: boolean;
29   markable?: boolean;
30   moderators: Array<CommunityUser>;
31   admins: Array<UserView>;
32 }
33
34 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
35
36   private emptyState: CommentNodeState = {
37     showReply: false,
38     showEdit: false,
39     showRemoveDialog: false,
40     removeReason: null,
41     showBanDialog: false,
42     banReason: null,
43     banExpires: null,
44     banType: BanType.Community
45   }
46
47   constructor(props: any, context: any) {
48     super(props, context);
49
50     this.state = this.emptyState;
51     this.handleReplyCancel = this.handleReplyCancel.bind(this);
52     this.handleCommentLike = this.handleCommentLike.bind(this);
53     this.handleCommentDisLike = this.handleCommentDisLike.bind(this);
54   }
55
56   render() {
57     let node = this.props.node;
58     return (
59       <div className={`comment ${node.comment.parent_id  && !this.props.noIndent ? 'ml-4' : ''}`}>
60         <div className={`mr-1 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
61           <div className={`pointer ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>
62             <svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
63           </div>
64           <div class={`font-weight-bold text-muted`}>{node.comment.score}</div>
65           <div className={`pointer ${node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>
66             <svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
67           </div>
68         </div>
69         <div id={`comment-${node.comment.id}`} className={`details ml-4 ${this.isCommentNew ? 'mark' : ''}`}>
70           <ul class="list-inline mb-0 text-muted small">
71             <li className="list-inline-item">
72               <Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link>
73             </li>
74             {this.isMod && 
75               <li className="list-inline-item badge badge-light">mod</li>
76             }
77             {this.isAdmin && 
78               <li className="list-inline-item badge badge-light">admin</li>
79             }
80             <li className="list-inline-item">
81               <span>(
82                 <span className="text-info">+{node.comment.upvotes}</span>
83                 <span> | </span>
84                 <span className="text-danger">-{node.comment.downvotes}</span>
85                 <span>) </span>
86               </span>
87             </li>
88             <li className="list-inline-item">
89               <span><MomentTime data={node.comment} /></span>
90             </li>
91           </ul>
92           {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
93           {!this.state.showEdit &&
94             <div>
95               <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.deleted ? '*deleted*' : node.comment.content)} />
96               <ul class="list-inline mb-1 text-muted small font-weight-bold">
97                 {UserService.Instance.user && !this.props.viewOnly && 
98                   <>
99                     <li className="list-inline-item">
100                       <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
101                     </li>
102                     <li className="list-inline-item mr-2">
103                       <span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? 'unsave' : 'save'}</span>
104                     </li>
105                     {this.myComment && 
106                       <>
107                         <li className="list-inline-item">
108                           <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
109                         </li>
110                         <li className="list-inline-item">
111                           <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
112                             {!this.props.node.comment.deleted ? 'delete' : 'restore'}
113                           </span>
114                         </li>
115                       </>
116                     }
117                     {/* Admins and mods can remove comments */}
118                     {this.canMod && 
119                       <li className="list-inline-item">
120                         {!this.props.node.comment.removed ? 
121                         <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
122                         <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
123                         }
124                       </li>
125                     }
126                     {/* Mods can ban from community, and appoint as mods to community */}
127                     {this.canMod &&
128                       <>
129                         {!this.isMod && 
130                           <li className="list-inline-item">
131                             {!this.props.node.comment.banned_from_community ? 
132                             <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> :
133                             <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span>
134                             }
135                           </li>
136                         }
137                         {!this.props.node.comment.banned_from_community &&
138                           <li className="list-inline-item">
139                             <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
140                           </li>
141                         }
142                       </>
143                     }
144                     {/* Admins can ban from all, and appoint other admins */}
145                     {this.canAdmin &&
146                       <>
147                         {!this.isAdmin && 
148                           <li className="list-inline-item">
149                             {!this.props.node.comment.banned ? 
150                             <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban from site</span> :
151                             <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban from site</span>
152                             }
153                           </li>
154                         }
155                         {!this.props.node.comment.banned &&
156                           <li className="list-inline-item">
157                             <span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
158                           </li>
159                         }
160                       </>
161                     }
162                   </>
163                 }
164                 <li className="list-inline-item">
165                   <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}>link</Link>
166                 </li>
167                 {this.props.markable && 
168                   <li className="list-inline-item">
169                     <span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{`mark as ${node.comment.read ? 'unread' : 'read'}`}</span>
170                   </li>
171                 }
172               </ul>
173             </div>
174           }
175         </div>
176         {this.state.showRemoveDialog && 
177           <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
178             <input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
179             <button type="submit" class="btn btn-secondary">Remove Comment</button>
180           </form>
181         }
182         {this.state.showBanDialog && 
183           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
184             <div class="form-group row">
185               <label class="col-form-label">Reason</label>
186               <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
187             </div>
188             {/* TODO hold off on expires until later */}
189             {/* <div class="form-group row"> */}
190             {/*   <label class="col-form-label">Expires</label> */}
191             {/*   <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
192             {/* </div> */}
193             <div class="form-group row">
194               <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
195             </div>
196           </form>
197         }
198         {this.state.showReply && 
199           <CommentForm 
200             node={node} 
201             onReplyCancel={this.handleReplyCancel} 
202             disabled={this.props.locked} 
203           />
204         }
205         {this.props.node.children && 
206           <CommentNodes 
207             nodes={this.props.node.children} 
208             locked={this.props.locked} 
209             moderators={this.props.moderators}
210             admins={this.props.admins}
211           />
212         }
213       </div>
214     )
215   }
216
217   get myComment(): boolean {
218     return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
219   }
220
221   get isMod(): boolean {
222     return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
223   }
224
225   get isAdmin(): boolean {
226     return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
227   }
228
229   get canMod(): boolean {
230
231     if (this.props.admins && this.props.moderators) {
232       let adminsThenMods = this.props.admins.map(a => a.id)
233       .concat(this.props.moderators.map(m => m.user_id));
234
235       return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
236       } else { 
237       return false;
238     }
239   }
240
241   get canAdmin(): boolean {
242     return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
243   }
244
245   handleReplyClick(i: CommentNode) {
246     i.state.showReply = true;
247     i.setState(i.state);
248   }
249
250   handleEditClick(i: CommentNode) {
251     i.state.showEdit = true;
252     i.setState(i.state);
253   }
254
255   handleDeleteClick(i: CommentNode) {
256     let deleteForm: CommentFormI = {
257       content: i.props.node.comment.content,
258       edit_id: i.props.node.comment.id,
259       creator_id: i.props.node.comment.creator_id,
260       post_id: i.props.node.comment.post_id,
261       parent_id: i.props.node.comment.parent_id,
262       deleted: !i.props.node.comment.deleted,
263       auth: null
264     };
265     WebSocketService.Instance.editComment(deleteForm);
266   }
267
268   handleSaveCommentClick(i: CommentNode) {
269     let saved = (i.props.node.comment.saved == undefined) ? true : !i.props.node.comment.saved;
270     let form: SaveCommentForm = {
271       comment_id: i.props.node.comment.id,
272       save: saved
273     };
274
275     WebSocketService.Instance.saveComment(form);
276   }
277
278   handleReplyCancel() {
279     this.state.showReply = false;
280     this.state.showEdit = false;
281     this.setState(this.state);
282   }
283
284
285   handleCommentLike(i: CommentNodeI) {
286
287     let form: CommentLikeForm = {
288       comment_id: i.comment.id,
289       post_id: i.comment.post_id,
290       score: (i.comment.my_vote == 1) ? 0 : 1
291     };
292     WebSocketService.Instance.likeComment(form);
293   }
294
295   handleCommentDisLike(i: CommentNodeI) {
296     let form: CommentLikeForm = {
297       comment_id: i.comment.id,
298       post_id: i.comment.post_id,
299       score: (i.comment.my_vote == -1) ? 0 : -1
300     };
301     WebSocketService.Instance.likeComment(form);
302   }
303
304   handleModRemoveShow(i: CommentNode) {
305     i.state.showRemoveDialog = true;
306     i.setState(i.state);
307   }
308
309   handleModRemoveReasonChange(i: CommentNode, event: any) {
310     i.state.removeReason = event.target.value;
311     i.setState(i.state);
312   }
313
314   handleModRemoveSubmit(i: CommentNode) {
315     event.preventDefault();
316     let form: CommentFormI = {
317       content: i.props.node.comment.content,
318       edit_id: i.props.node.comment.id,
319       creator_id: i.props.node.comment.creator_id,
320       post_id: i.props.node.comment.post_id,
321       parent_id: i.props.node.comment.parent_id,
322       removed: !i.props.node.comment.removed,
323       reason: i.state.removeReason,
324       auth: null
325     };
326     WebSocketService.Instance.editComment(form);
327
328     i.state.showRemoveDialog = false;
329     i.setState(i.state);
330   }
331
332   handleMarkRead(i: CommentNode) {
333     let form: CommentFormI = {
334       content: i.props.node.comment.content,
335       edit_id: i.props.node.comment.id,
336       creator_id: i.props.node.comment.creator_id,
337       post_id: i.props.node.comment.post_id,
338       parent_id: i.props.node.comment.parent_id,
339       read: !i.props.node.comment.read,
340       auth: null
341     };
342     WebSocketService.Instance.editComment(form);
343   }
344
345
346   handleModBanFromCommunityShow(i: CommentNode) {
347     i.state.showBanDialog = true;
348     i.state.banType = BanType.Community;
349     i.setState(i.state);
350   }
351
352   handleModBanShow(i: CommentNode) {
353     i.state.showBanDialog = true;
354     i.state.banType = BanType.Site;
355     i.setState(i.state);
356   }
357
358   handleModBanReasonChange(i: CommentNode, event: any) {
359     i.state.banReason = event.target.value;
360     i.setState(i.state);
361   }
362
363   handleModBanExpiresChange(i: CommentNode, event: any) {
364     i.state.banExpires = event.target.value;
365     i.setState(i.state);
366   }
367
368   handleModBanFromCommunitySubmit(i: CommentNode) {
369     i.state.banType = BanType.Community;
370     i.setState(i.state);
371     i.handleModBanBothSubmit(i);
372   }
373
374   handleModBanSubmit(i: CommentNode) {
375     i.state.banType = BanType.Site;
376     i.setState(i.state);
377     i.handleModBanBothSubmit(i);
378   }
379
380   handleModBanBothSubmit(i: CommentNode) {
381     event.preventDefault();
382
383     console.log(BanType[i.state.banType]);
384     console.log(i.props.node.comment.banned);
385
386     if (i.state.banType == BanType.Community) {
387       let form: BanFromCommunityForm = {
388         user_id: i.props.node.comment.creator_id,
389         community_id: i.props.node.comment.community_id,
390         ban: !i.props.node.comment.banned_from_community,
391         reason: i.state.banReason,
392         expires: getUnixTime(i.state.banExpires),
393       };
394       WebSocketService.Instance.banFromCommunity(form);
395     } else {
396       let form: BanUserForm = {
397         user_id: i.props.node.comment.creator_id,
398         ban: !i.props.node.comment.banned,
399         reason: i.state.banReason,
400         expires: getUnixTime(i.state.banExpires),
401       };
402       WebSocketService.Instance.banUser(form);
403     }
404
405     i.state.showBanDialog = false;
406     i.setState(i.state);
407   }
408
409   handleAddModToCommunity(i: CommentNode) {
410     let form: AddModToCommunityForm = {
411       user_id: i.props.node.comment.creator_id,
412       community_id: i.props.node.comment.community_id,
413       added: !i.isMod,
414     };
415     WebSocketService.Instance.addModToCommunity(form);
416     i.setState(i.state);
417   }
418
419   handleAddAdmin(i: CommentNode) {
420     let form: AddAdminForm = {
421       user_id: i.props.node.comment.creator_id,
422       added: !i.isAdmin,
423     };
424     WebSocketService.Instance.addAdmin(form);
425     i.setState(i.state);
426   }
427
428   get isCommentNew(): boolean {
429     let now = moment.utc().subtract(10, 'minutes');
430     let then = moment.utc(this.props.node.comment.published);
431     return now.isBefore(then);
432   }
433 }