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, BanType } 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';
13 interface CommentNodeState {
16 showRemoveDialog: boolean;
18 showBanDialog: boolean;
22 showConfirmTransferSite: boolean;
23 showConfirmTransferCommunity: boolean;
28 interface CommentNodeProps {
34 moderators: Array<CommunityUser>;
35 admins: Array<UserView>;
36 postCreatorId?: number;
39 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
41 private emptyState: CommentNodeState = {
44 showRemoveDialog: false,
49 banType: BanType.Community,
52 showConfirmTransferSite: false,
53 showConfirmTransferCommunity: false,
56 constructor(props: any, context: any) {
57 super(props, context);
59 this.state = this.emptyState;
60 this.handleReplyCancel = this.handleReplyCancel.bind(this);
61 this.handleCommentLike = this.handleCommentLike.bind(this);
62 this.handleCommentDisLike = this.handleCommentDisLike.bind(this);
66 let node = this.props.node;
68 <div className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}>
69 {!this.state.collapsed &&
70 <div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
71 <button className={`btn p-0 ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>
72 <svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
74 <div class={`font-weight-bold text-muted`}>{node.comment.score}</div>
75 <button className={`btn p-0 ${node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>
76 <svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
80 <div id={`comment-${node.comment.id}`} className={`details comment-node ml-4 ${this.isCommentNew ? 'mark' : ''}`}>
81 <ul class="list-inline mb-0 text-muted small">
82 <li className="list-inline-item">
83 <Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link>
86 <li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li>
89 <li className="list-inline-item badge badge-light"><T i18nKey="admin">#</T></li>
91 {this.isPostCreator &&
92 <li className="list-inline-item badge badge-light"><T i18nKey="creator">#</T></li>
94 {(node.comment.banned_from_community || node.comment.banned) &&
95 <li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
97 <li className="list-inline-item">
99 <span className="text-info">+{node.comment.upvotes}</span>
101 <span className="text-danger">-{node.comment.downvotes}</span>
105 <li className="list-inline-item">
106 <span><MomentTime data={node.comment} /></span>
108 <li className="list-inline-item">
109 <div className="pointer text-monospace" onClick={linkEvent(this, this.handleCommentCollapse)}>{this.state.collapsed ? '[+]' : '[-]'}</div>
112 {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
113 {!this.state.showEdit && !this.state.collapsed &&
115 {this.state.viewSource ? <pre>{this.commentUnlessRemoved}</pre> :
116 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)} />
118 <ul class="list-inline mb-1 text-muted small font-weight-bold">
119 {UserService.Instance.user && !this.props.viewOnly &&
121 <li className="list-inline-item">
122 <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}><T i18nKey="reply">#</T></span>
124 <li className="list-inline-item mr-2">
125 <span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}</span>
129 <li className="list-inline-item">
130 <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
132 <li className="list-inline-item">
133 <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
134 {!node.comment.deleted ? i18n.t('delete') : i18n.t('restore')}
139 {/* Admins and mods can remove comments */}
140 {(this.canMod || this.canAdmin) &&
141 <li className="list-inline-item">
142 {!node.comment.removed ?
143 <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
144 <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
148 {/* Mods can ban from community, and appoint as mods to community */}
152 <li className="list-inline-item">
153 {!node.comment.banned_from_community ?
154 <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> :
155 <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span>
159 {!node.comment.banned_from_community &&
160 <li className="list-inline-item">
161 <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span>
166 {/* Community creators and admins can transfer community to another mod */}
167 {(this.amCommunityCreator || this.canAdmin) && this.isMod &&
168 <li className="list-inline-item">
169 {!this.state.showConfirmTransferCommunity ?
170 <span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
172 <span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
173 <span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
174 <span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
179 {/* Admins can ban from all, and appoint other admins */}
183 <li className="list-inline-item">
184 {!node.comment.banned ?
185 <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> :
186 <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span>
190 {!node.comment.banned &&
191 <li className="list-inline-item">
192 <span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span>
197 {/* Site Creator can transfer to another admin */}
198 {this.amSiteCreator && this.isAdmin &&
199 <li className="list-inline-item">
200 {!this.state.showConfirmTransferSite ?
201 <span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
203 <span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
204 <span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
205 <span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
212 <li className="list-inline-item">
213 <span className="pointer" onClick={linkEvent(this, this.handleViewSource)}><T i18nKey="view_source">#</T></span>
215 <li className="list-inline-item">
216 <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}><T i18nKey="link">#</T></Link>
218 {this.props.markable &&
219 <li className="list-inline-item">
220 <span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{node.comment.read ? i18n.t('mark_as_unread') : i18n.t('mark_as_read')}</span>
227 {this.state.showRemoveDialog &&
228 <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
229 <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
230 <button type="submit" class="btn btn-secondary"><T i18nKey="remove_comment">#</T></button>
233 {this.state.showBanDialog &&
234 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
235 <div class="form-group row">
236 <label class="col-form-label"><T i18nKey="reason">#</T></label>
237 <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
239 {/* TODO hold off on expires until later */}
240 {/* <div class="form-group row"> */}
241 {/* <label class="col-form-label">Expires</label> */}
242 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
244 <div class="form-group row">
245 <button type="submit" class="btn btn-secondary">{i18n.t('ban')} {node.comment.creator_name}</button>
249 {this.state.showReply &&
252 onReplyCancel={this.handleReplyCancel}
253 disabled={this.props.locked}
256 {node.children && !this.state.collapsed &&
258 nodes={node.children}
259 locked={this.props.locked}
260 moderators={this.props.moderators}
261 admins={this.props.admins}
262 postCreatorId={this.props.postCreatorId}
265 {/* A collapsed clearfix */}
266 {this.state.collapsed && <div class="row col-12"></div>}
271 get myComment(): boolean {
272 return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
275 get isMod(): boolean {
276 return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
279 get isAdmin(): boolean {
280 return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
283 get isPostCreator(): boolean {
284 return this.props.node.comment.creator_id == this.props.postCreatorId;
287 get canMod(): boolean {
289 if (this.props.admins && this.props.moderators) {
290 let adminsThenMods = this.props.admins.map(a => a.id)
291 .concat(this.props.moderators.map(m => m.user_id));
293 return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
299 get canAdmin(): boolean {
300 return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
303 get amCommunityCreator(): boolean {
304 return this.props.moderators &&
305 UserService.Instance.user &&
306 (this.props.node.comment.creator_id != UserService.Instance.user.id) &&
307 (UserService.Instance.user.id == this.props.moderators[0].user_id);
310 get amSiteCreator(): boolean {
311 return this.props.admins &&
312 UserService.Instance.user &&
313 (this.props.node.comment.creator_id != UserService.Instance.user.id) &&
314 (UserService.Instance.user.id == this.props.admins[0].id);
317 get commentUnlessRemoved(): string {
318 let node = this.props.node;
319 return node.comment.removed ? `*${i18n.t('removed')}*` : node.comment.deleted ? `*${i18n.t('deleted')}*` : node.comment.content;
322 handleReplyClick(i: CommentNode) {
323 i.state.showReply = true;
327 handleEditClick(i: CommentNode) {
328 i.state.showEdit = true;
332 handleDeleteClick(i: CommentNode) {
333 let deleteForm: 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 deleted: !i.props.node.comment.deleted,
342 WebSocketService.Instance.editComment(deleteForm);
345 handleSaveCommentClick(i: CommentNode) {
346 let saved = (i.props.node.comment.saved == undefined) ? true : !i.props.node.comment.saved;
347 let form: SaveCommentForm = {
348 comment_id: i.props.node.comment.id,
352 WebSocketService.Instance.saveComment(form);
355 handleReplyCancel() {
356 this.state.showReply = false;
357 this.state.showEdit = false;
358 this.setState(this.state);
362 handleCommentLike(i: CommentNodeI) {
364 let form: CommentLikeForm = {
365 comment_id: i.comment.id,
366 post_id: i.comment.post_id,
367 score: (i.comment.my_vote == 1) ? 0 : 1
369 WebSocketService.Instance.likeComment(form);
372 handleCommentDisLike(i: CommentNodeI) {
373 let form: CommentLikeForm = {
374 comment_id: i.comment.id,
375 post_id: i.comment.post_id,
376 score: (i.comment.my_vote == -1) ? 0 : -1
378 WebSocketService.Instance.likeComment(form);
381 handleModRemoveShow(i: CommentNode) {
382 i.state.showRemoveDialog = true;
386 handleModRemoveReasonChange(i: CommentNode, event: any) {
387 i.state.removeReason = event.target.value;
391 handleModRemoveSubmit(i: CommentNode) {
392 event.preventDefault();
393 let form: CommentFormI = {
394 content: i.props.node.comment.content,
395 edit_id: i.props.node.comment.id,
396 creator_id: i.props.node.comment.creator_id,
397 post_id: i.props.node.comment.post_id,
398 parent_id: i.props.node.comment.parent_id,
399 removed: !i.props.node.comment.removed,
400 reason: i.state.removeReason,
403 WebSocketService.Instance.editComment(form);
405 i.state.showRemoveDialog = false;
409 handleMarkRead(i: CommentNode) {
410 let form: CommentFormI = {
411 content: i.props.node.comment.content,
412 edit_id: i.props.node.comment.id,
413 creator_id: i.props.node.comment.creator_id,
414 post_id: i.props.node.comment.post_id,
415 parent_id: i.props.node.comment.parent_id,
416 read: !i.props.node.comment.read,
419 WebSocketService.Instance.editComment(form);
423 handleModBanFromCommunityShow(i: CommentNode) {
424 i.state.showBanDialog = true;
425 i.state.banType = BanType.Community;
429 handleModBanShow(i: CommentNode) {
430 i.state.showBanDialog = true;
431 i.state.banType = BanType.Site;
435 handleModBanReasonChange(i: CommentNode, event: any) {
436 i.state.banReason = event.target.value;
440 handleModBanExpiresChange(i: CommentNode, event: any) {
441 i.state.banExpires = event.target.value;
445 handleModBanFromCommunitySubmit(i: CommentNode) {
446 i.state.banType = BanType.Community;
448 i.handleModBanBothSubmit(i);
451 handleModBanSubmit(i: CommentNode) {
452 i.state.banType = BanType.Site;
454 i.handleModBanBothSubmit(i);
457 handleModBanBothSubmit(i: CommentNode) {
458 event.preventDefault();
460 if (i.state.banType == BanType.Community) {
461 let form: BanFromCommunityForm = {
462 user_id: i.props.node.comment.creator_id,
463 community_id: i.props.node.comment.community_id,
464 ban: !i.props.node.comment.banned_from_community,
465 reason: i.state.banReason,
466 expires: getUnixTime(i.state.banExpires),
468 WebSocketService.Instance.banFromCommunity(form);
470 let form: BanUserForm = {
471 user_id: i.props.node.comment.creator_id,
472 ban: !i.props.node.comment.banned,
473 reason: i.state.banReason,
474 expires: getUnixTime(i.state.banExpires),
476 WebSocketService.Instance.banUser(form);
479 i.state.showBanDialog = false;
483 handleAddModToCommunity(i: CommentNode) {
484 let form: AddModToCommunityForm = {
485 user_id: i.props.node.comment.creator_id,
486 community_id: i.props.node.comment.community_id,
489 WebSocketService.Instance.addModToCommunity(form);
493 handleAddAdmin(i: CommentNode) {
494 let form: AddAdminForm = {
495 user_id: i.props.node.comment.creator_id,
498 WebSocketService.Instance.addAdmin(form);
502 handleShowConfirmTransferCommunity(i: CommentNode) {
503 i.state.showConfirmTransferCommunity = true;
507 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
508 i.state.showConfirmTransferCommunity = false;
512 handleTransferCommunity(i: CommentNode) {
513 let form: TransferCommunityForm = {
514 community_id: i.props.node.comment.community_id,
515 user_id: i.props.node.comment.creator_id,
517 WebSocketService.Instance.transferCommunity(form);
518 i.state.showConfirmTransferCommunity = false;
522 handleShowConfirmTransferSite(i: CommentNode) {
523 i.state.showConfirmTransferSite = true;
527 handleCancelShowConfirmTransferSite(i: CommentNode) {
528 i.state.showConfirmTransferSite = false;
532 handleTransferSite(i: CommentNode) {
533 let form: TransferSiteForm = {
534 user_id: i.props.node.comment.creator_id,
536 WebSocketService.Instance.transferSite(form);
537 i.state.showConfirmTransferSite = false;
541 get isCommentNew(): boolean {
542 let now = moment.utc().subtract(10, 'minutes');
543 let then = moment.utc(this.props.node.comment.published);
544 return now.isBefore(then);
547 handleCommentCollapse(i: CommentNode) {
548 i.state.collapsed = !i.state.collapsed;
552 handleViewSource(i: CommentNode) {
553 i.state.viewSource = !i.state.viewSource;