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