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';
13 enum BanType {Community, Site};
15 interface CommentNodeState {
18 showRemoveDialog: boolean;
20 showBanDialog: boolean;
25 showConfirmTransferSite: boolean;
26 showConfirmTransferCommunity: boolean;
29 interface CommentNodeProps {
35 moderators: Array<CommunityUser>;
36 admins: Array<UserView>;
39 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
41 private emptyState: CommentNodeState = {
44 showRemoveDialog: false,
49 banType: BanType.Community,
51 showConfirmTransferSite: false,
52 showConfirmTransferCommunity: false,
55 constructor(props: any, context: any) {
56 super(props, context);
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);
65 let node = this.props.node;
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>
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>
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>
83 <li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li>
86 <li className="list-inline-item badge badge-light"><T i18nKey="admin">#</T></li>
88 <li className="list-inline-item">
90 <span className="text-info">+{node.comment.upvotes}</span>
92 <span className="text-danger">-{node.comment.downvotes}</span>
96 <li className="list-inline-item">
97 <span><MomentTime data={node.comment} /></span>
99 <li className="list-inline-item">
100 <div className="pointer text-monospace" onClick={linkEvent(this, this.handleCommentCollapse)}>{this.state.collapsed ? '[+]' : '[-]'}</div>
103 {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
104 {!this.state.showEdit && !this.state.collapsed &&
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 &&
110 <li className="list-inline-item">
111 <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}><T i18nKey="reply">#</T></span>
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>
118 <li className="list-inline-item">
119 <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
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')}
128 {/* Admins and mods can remove comments */}
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>
137 {/* Mods can ban from community, and appoint as mods to community */}
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>
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>
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>
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>
168 {/* Admins can ban from all, and appoint other admins */}
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>
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>
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>
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>
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>
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>
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>
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)} />
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)} /> */}
230 <div class="form-group row">
231 <button type="submit" class="btn btn-secondary">{i18n.t('ban')} {this.props.node.comment.creator_name}</button>
235 {this.state.showReply &&
238 onReplyCancel={this.handleReplyCancel}
239 disabled={this.props.locked}
242 {this.props.node.children && !this.state.collapsed &&
244 nodes={this.props.node.children}
245 locked={this.props.locked}
246 moderators={this.props.moderators}
247 admins={this.props.admins}
250 {/* A collapsed clearfix */}
251 {this.state.collapsed && <div class="row col-12"></div>}
256 get myComment(): boolean {
257 return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
260 get isMod(): boolean {
261 return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
264 get isAdmin(): boolean {
265 return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
268 get canMod(): boolean {
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));
274 return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
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);
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);
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);
298 handleReplyClick(i: CommentNode) {
299 i.state.showReply = true;
303 handleEditClick(i: CommentNode) {
304 i.state.showEdit = true;
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,
318 WebSocketService.Instance.editComment(deleteForm);
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,
328 WebSocketService.Instance.saveComment(form);
331 handleReplyCancel() {
332 this.state.showReply = false;
333 this.state.showEdit = false;
334 this.setState(this.state);
338 handleCommentLike(i: CommentNodeI) {
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
345 WebSocketService.Instance.likeComment(form);
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
354 WebSocketService.Instance.likeComment(form);
357 handleModRemoveShow(i: CommentNode) {
358 i.state.showRemoveDialog = true;
362 handleModRemoveReasonChange(i: CommentNode, event: any) {
363 i.state.removeReason = event.target.value;
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,
379 WebSocketService.Instance.editComment(form);
381 i.state.showRemoveDialog = false;
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,
395 WebSocketService.Instance.editComment(form);
399 handleModBanFromCommunityShow(i: CommentNode) {
400 i.state.showBanDialog = true;
401 i.state.banType = BanType.Community;
405 handleModBanShow(i: CommentNode) {
406 i.state.showBanDialog = true;
407 i.state.banType = BanType.Site;
411 handleModBanReasonChange(i: CommentNode, event: any) {
412 i.state.banReason = event.target.value;
416 handleModBanExpiresChange(i: CommentNode, event: any) {
417 i.state.banExpires = event.target.value;
421 handleModBanFromCommunitySubmit(i: CommentNode) {
422 i.state.banType = BanType.Community;
424 i.handleModBanBothSubmit(i);
427 handleModBanSubmit(i: CommentNode) {
428 i.state.banType = BanType.Site;
430 i.handleModBanBothSubmit(i);
433 handleModBanBothSubmit(i: CommentNode) {
434 event.preventDefault();
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),
444 WebSocketService.Instance.banFromCommunity(form);
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),
452 WebSocketService.Instance.banUser(form);
455 i.state.showBanDialog = false;
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,
465 WebSocketService.Instance.addModToCommunity(form);
469 handleAddAdmin(i: CommentNode) {
470 let form: AddAdminForm = {
471 user_id: i.props.node.comment.creator_id,
474 WebSocketService.Instance.addAdmin(form);
478 handleShowConfirmTransferCommunity(i: CommentNode) {
479 i.state.showConfirmTransferCommunity = true;
483 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
484 i.state.showConfirmTransferCommunity = false;
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,
493 WebSocketService.Instance.transferCommunity(form);
497 handleShowConfirmTransferSite(i: CommentNode) {
498 i.state.showConfirmTransferSite = true;
502 handleCancelShowConfirmTransferSite(i: CommentNode) {
503 i.state.showConfirmTransferSite = false;
507 handleTransferSite(i: CommentNode) {
508 let form: TransferSiteForm = {
509 user_id: i.props.node.comment.creator_id,
511 WebSocketService.Instance.transferSite(form);
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);
521 handleCommentCollapse(i: CommentNode) {
522 i.state.collapsed = !i.state.collapsed;