1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
4 CommentNode as CommentNodeI,
6 CommentForm as CommentFormI,
13 AddModToCommunityForm,
15 TransferCommunityForm,
18 } from '../interfaces';
19 import { WebSocketService, UserService } from '../services';
25 pictshareAvatarThumbnail,
28 import moment from 'moment';
29 import { MomentTime } from './moment-time';
30 import { CommentForm } from './comment-form';
31 import { CommentNodes } from './comment-nodes';
32 import { i18n } from '../i18next';
34 interface CommentNodeState {
37 showRemoveDialog: boolean;
39 showBanDialog: boolean;
43 showConfirmTransferSite: boolean;
44 showConfirmTransferCommunity: boolean;
45 showConfirmAppointAsMod: boolean;
46 showConfirmAppointAsAdmin: boolean;
49 upvoteLoading: boolean;
50 downvoteLoading: boolean;
53 interface CommentNodeProps {
59 moderators: Array<CommunityUser>;
60 admins: Array<UserView>;
61 postCreatorId?: number;
64 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
65 private emptyState: CommentNodeState = {
68 showRemoveDialog: false,
73 banType: BanType.Community,
76 showConfirmTransferSite: false,
77 showConfirmTransferCommunity: false,
78 showConfirmAppointAsMod: false,
79 showConfirmAppointAsAdmin: false,
80 upvoteLoading: this.props.node.comment.upvoteLoading,
81 downvoteLoading: this.props.node.comment.downvoteLoading,
84 constructor(props: any, context: any) {
85 super(props, context);
87 this.state = this.emptyState;
88 this.handleReplyCancel = this.handleReplyCancel.bind(this);
89 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
90 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
93 componentWillReceiveProps(nextProps: CommentNodeProps) {
95 nextProps.node.comment.upvoteLoading !== this.state.upvoteLoading ||
96 nextProps.node.comment.downvoteLoading !== this.state.downvoteLoading
100 downvoteLoading: false,
106 let node = this.props.node;
109 className={`comment ${
110 node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
113 {!this.state.collapsed && (
115 className={`vote-bar mr-2 float-left small text-center ${this.props
116 .viewOnly && 'no-click'}`}
119 className={`btn btn-link p-0 ${
120 node.comment.my_vote == 1 ? 'text-info' : 'text-muted'
122 onClick={linkEvent(node, this.handleCommentUpvote)}
124 {this.state.upvoteLoading ? (
125 <svg class="icon icon-spinner spin upvote">
126 <use xlinkHref="#icon-spinner"></use>
129 <svg class="icon upvote">
130 <use xlinkHref="#icon-arrow-up"></use>
134 <div class={`font-weight-bold text-muted`}>
137 {WebSocketService.Instance.site.enable_downvotes && (
139 className={`btn btn-link p-0 ${
140 node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
142 onClick={linkEvent(node, this.handleCommentDownvote)}
144 {this.state.downvoteLoading ? (
145 <svg class="icon icon-spinner spin downvote">
146 <use xlinkHref="#icon-spinner"></use>
149 <svg class="icon downvote">
150 <use xlinkHref="#icon-arrow-down"></use>
158 id={`comment-${node.comment.id}`}
159 className={`details comment-node ml-4 ${
160 this.isCommentNew ? 'mark' : ''
163 <ul class="list-inline mb-0 text-muted small">
164 <li className="list-inline-item">
166 className="text-info"
167 to={`/u/${node.comment.creator_name}`}
169 {node.comment.creator_avatar && showAvatars() && (
173 src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
174 class="rounded-circle mr-1"
177 <span>{node.comment.creator_name}</span>
181 <li className="list-inline-item badge badge-light">
186 <li className="list-inline-item badge badge-light">
190 {this.isPostCreator && (
191 <li className="list-inline-item badge badge-light">
195 {(node.comment.banned_from_community || node.comment.banned) && (
196 <li className="list-inline-item badge badge-danger">
200 <li className="list-inline-item">
202 (<span className="text-info">+{node.comment.upvotes}</span>
204 <span className="text-danger">-{node.comment.downvotes}</span>
208 <li className="list-inline-item">
210 <MomentTime data={node.comment} />
213 <li className="list-inline-item">
215 className="pointer text-monospace"
216 onClick={linkEvent(this, this.handleCommentCollapse)}
218 {this.state.collapsed ? '[+]' : '[-]'}
222 {this.state.showEdit && (
226 onReplyCancel={this.handleReplyCancel}
227 disabled={this.props.locked}
230 {!this.state.showEdit && !this.state.collapsed && (
232 {this.state.viewSource ? (
233 <pre>{this.commentUnlessRemoved}</pre>
237 dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
240 <ul class="list-inline mb-1 text-muted small font-weight-bold">
241 {this.props.markable && (
242 <li className="list-inline-item">
245 onClick={linkEvent(this, this.handleMarkRead)}
248 ? i18n.t('mark_as_unread')
249 : i18n.t('mark_as_read')}
253 {UserService.Instance.user && !this.props.viewOnly && (
255 <li className="list-inline-item">
258 onClick={linkEvent(this, this.handleReplyClick)}
263 <li className="list-inline-item mr-2">
266 onClick={linkEvent(this, this.handleSaveCommentClick)}
268 {node.comment.saved ? i18n.t('unsave') : i18n.t('save')}
273 <li className="list-inline-item">
276 onClick={linkEvent(this, this.handleEditClick)}
281 <li className="list-inline-item">
284 onClick={linkEvent(this, this.handleDeleteClick)}
286 {!node.comment.deleted
293 {!this.myComment && (
294 <li className="list-inline-item">
297 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
299 {i18n.t('message').toLowerCase()}
303 <li className="list-inline-item">•</li>
304 <li className="list-inline-item">
307 onClick={linkEvent(this, this.handleViewSource)}
309 {i18n.t('view_source')}
312 <li className="list-inline-item">
314 className="text-muted"
315 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
320 {/* Admins and mods can remove comments */}
321 {(this.canMod || this.canAdmin) && (
323 <li className="list-inline-item">•</li>
324 <li className="list-inline-item">
325 {!node.comment.removed ? (
330 this.handleModRemoveShow
340 this.handleModRemoveSubmit
349 {/* Mods can ban from community, and appoint as mods to community */}
353 <li className="list-inline-item">
354 {!node.comment.banned_from_community ? (
359 this.handleModBanFromCommunityShow
369 this.handleModBanFromCommunitySubmit
377 {!node.comment.banned_from_community && (
378 <li className="list-inline-item">
379 {!this.state.showConfirmAppointAsMod ? (
384 this.handleShowConfirmAppointAsMod
388 ? i18n.t('remove_as_mod')
389 : i18n.t('appoint_as_mod')}
393 <span class="d-inline-block mr-1">
394 {i18n.t('are_you_sure')}
397 class="pointer d-inline-block mr-1"
400 this.handleAddModToCommunity
406 class="pointer d-inline-block"
409 this.handleCancelConfirmAppointAsMod
420 {/* Community creators and admins can transfer community to another mod */}
421 {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
422 <li className="list-inline-item">
423 {!this.state.showConfirmTransferCommunity ? (
428 this.handleShowConfirmTransferCommunity
431 {i18n.t('transfer_community')}
435 <span class="d-inline-block mr-1">
436 {i18n.t('are_you_sure')}
439 class="pointer d-inline-block mr-1"
442 this.handleTransferCommunity
448 class="pointer d-inline-block"
451 this.handleCancelShowConfirmTransferCommunity
460 {/* Admins can ban from all, and appoint other admins */}
464 <li className="list-inline-item">
465 {!node.comment.banned ? (
468 onClick={linkEvent(this, this.handleModBanShow)}
470 {i18n.t('ban_from_site')}
477 this.handleModBanSubmit
480 {i18n.t('unban_from_site')}
485 {!node.comment.banned && (
486 <li className="list-inline-item">
487 {!this.state.showConfirmAppointAsAdmin ? (
492 this.handleShowConfirmAppointAsAdmin
496 ? i18n.t('remove_as_admin')
497 : i18n.t('appoint_as_admin')}
501 <span class="d-inline-block mr-1">
502 {i18n.t('are_you_sure')}
505 class="pointer d-inline-block mr-1"
506 onClick={linkEvent(this, this.handleAddAdmin)}
511 class="pointer d-inline-block"
514 this.handleCancelConfirmAppointAsAdmin
525 {/* Site Creator can transfer to another admin */}
526 {this.amSiteCreator && this.isAdmin && (
527 <li className="list-inline-item">
528 {!this.state.showConfirmTransferSite ? (
533 this.handleShowConfirmTransferSite
536 {i18n.t('transfer_site')}
540 <span class="d-inline-block mr-1">
541 {i18n.t('are_you_sure')}
544 class="pointer d-inline-block mr-1"
545 onClick={linkEvent(this, this.handleTransferSite)}
550 class="pointer d-inline-block"
553 this.handleCancelShowConfirmTransferSite
568 {this.state.showRemoveDialog && (
571 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
575 class="form-control mr-2"
576 placeholder={i18n.t('reason')}
577 value={this.state.removeReason}
578 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
580 <button type="submit" class="btn btn-secondary">
581 {i18n.t('remove_comment')}
585 {this.state.showBanDialog && (
586 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
587 <div class="form-group row">
588 <label class="col-form-label">{i18n.t('reason')}</label>
591 class="form-control mr-2"
592 placeholder={i18n.t('reason')}
593 value={this.state.banReason}
594 onInput={linkEvent(this, this.handleModBanReasonChange)}
597 {/* TODO hold off on expires until later */}
598 {/* <div class="form-group row"> */}
599 {/* <label class="col-form-label">Expires</label> */}
600 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
602 <div class="form-group row">
603 <button type="submit" class="btn btn-secondary">
604 {i18n.t('ban')} {node.comment.creator_name}
609 {this.state.showReply && (
612 onReplyCancel={this.handleReplyCancel}
613 disabled={this.props.locked}
616 {node.children && !this.state.collapsed && (
618 nodes={node.children}
619 locked={this.props.locked}
620 moderators={this.props.moderators}
621 admins={this.props.admins}
622 postCreatorId={this.props.postCreatorId}
625 {/* A collapsed clearfix */}
626 {this.state.collapsed && <div class="row col-12"></div>}
631 get myComment(): boolean {
633 UserService.Instance.user &&
634 this.props.node.comment.creator_id == UserService.Instance.user.id
638 get isMod(): boolean {
640 this.props.moderators &&
642 this.props.moderators.map(m => m.user_id),
643 this.props.node.comment.creator_id
648 get isAdmin(): boolean {
652 this.props.admins.map(a => a.id),
653 this.props.node.comment.creator_id
658 get isPostCreator(): boolean {
659 return this.props.node.comment.creator_id == this.props.postCreatorId;
662 get canMod(): boolean {
663 if (this.props.admins && this.props.moderators) {
664 let adminsThenMods = this.props.admins
666 .concat(this.props.moderators.map(m => m.user_id));
669 UserService.Instance.user,
671 this.props.node.comment.creator_id
678 get canAdmin(): boolean {
682 UserService.Instance.user,
683 this.props.admins.map(a => a.id),
684 this.props.node.comment.creator_id
689 get amCommunityCreator(): boolean {
691 this.props.moderators &&
692 UserService.Instance.user &&
693 this.props.node.comment.creator_id != UserService.Instance.user.id &&
694 UserService.Instance.user.id == this.props.moderators[0].user_id
698 get amSiteCreator(): boolean {
701 UserService.Instance.user &&
702 this.props.node.comment.creator_id != UserService.Instance.user.id &&
703 UserService.Instance.user.id == this.props.admins[0].id
707 get commentUnlessRemoved(): string {
708 let node = this.props.node;
709 return node.comment.removed
710 ? `*${i18n.t('removed')}*`
711 : node.comment.deleted
712 ? `*${i18n.t('deleted')}*`
713 : node.comment.content;
716 handleReplyClick(i: CommentNode) {
717 i.state.showReply = true;
721 handleEditClick(i: CommentNode) {
722 i.state.showEdit = true;
726 handleDeleteClick(i: CommentNode) {
727 let deleteForm: CommentFormI = {
728 content: i.props.node.comment.content,
729 edit_id: i.props.node.comment.id,
730 creator_id: i.props.node.comment.creator_id,
731 post_id: i.props.node.comment.post_id,
732 parent_id: i.props.node.comment.parent_id,
733 deleted: !i.props.node.comment.deleted,
736 WebSocketService.Instance.editComment(deleteForm);
739 handleSaveCommentClick(i: CommentNode) {
741 i.props.node.comment.saved == undefined
743 : !i.props.node.comment.saved;
744 let form: SaveCommentForm = {
745 comment_id: i.props.node.comment.id,
749 WebSocketService.Instance.saveComment(form);
752 handleReplyCancel() {
753 this.state.showReply = false;
754 this.state.showEdit = false;
755 this.setState(this.state);
758 handleCommentUpvote(i: CommentNodeI) {
759 if (UserService.Instance.user) {
764 let form: CommentLikeForm = {
765 comment_id: i.comment.id,
766 post_id: i.comment.post_id,
767 score: i.comment.my_vote == 1 ? 0 : 1,
769 WebSocketService.Instance.likeComment(form);
772 handleCommentDownvote(i: CommentNodeI) {
773 if (UserService.Instance.user) {
775 downvoteLoading: true,
778 let form: CommentLikeForm = {
779 comment_id: i.comment.id,
780 post_id: i.comment.post_id,
781 score: i.comment.my_vote == -1 ? 0 : -1,
783 WebSocketService.Instance.likeComment(form);
786 handleModRemoveShow(i: CommentNode) {
787 i.state.showRemoveDialog = true;
791 handleModRemoveReasonChange(i: CommentNode, event: any) {
792 i.state.removeReason = event.target.value;
796 handleModRemoveSubmit(i: CommentNode) {
797 event.preventDefault();
798 let form: CommentFormI = {
799 content: i.props.node.comment.content,
800 edit_id: i.props.node.comment.id,
801 creator_id: i.props.node.comment.creator_id,
802 post_id: i.props.node.comment.post_id,
803 parent_id: i.props.node.comment.parent_id,
804 removed: !i.props.node.comment.removed,
805 reason: i.state.removeReason,
808 WebSocketService.Instance.editComment(form);
810 i.state.showRemoveDialog = false;
814 handleMarkRead(i: CommentNode) {
815 // if it has a user_mention_id field, then its a mention
816 if (i.props.node.comment.user_mention_id) {
817 let form: EditUserMentionForm = {
818 user_mention_id: i.props.node.comment.user_mention_id,
819 read: !i.props.node.comment.read,
821 WebSocketService.Instance.editUserMention(form);
823 let form: CommentFormI = {
824 content: i.props.node.comment.content,
825 edit_id: i.props.node.comment.id,
826 creator_id: i.props.node.comment.creator_id,
827 post_id: i.props.node.comment.post_id,
828 parent_id: i.props.node.comment.parent_id,
829 read: !i.props.node.comment.read,
832 WebSocketService.Instance.editComment(form);
836 handleModBanFromCommunityShow(i: CommentNode) {
837 i.state.showBanDialog = !i.state.showBanDialog;
838 i.state.banType = BanType.Community;
842 handleModBanShow(i: CommentNode) {
843 i.state.showBanDialog = !i.state.showBanDialog;
844 i.state.banType = BanType.Site;
848 handleModBanReasonChange(i: CommentNode, event: any) {
849 i.state.banReason = event.target.value;
853 handleModBanExpiresChange(i: CommentNode, event: any) {
854 i.state.banExpires = event.target.value;
858 handleModBanFromCommunitySubmit(i: CommentNode) {
859 i.state.banType = BanType.Community;
861 i.handleModBanBothSubmit(i);
864 handleModBanSubmit(i: CommentNode) {
865 i.state.banType = BanType.Site;
867 i.handleModBanBothSubmit(i);
870 handleModBanBothSubmit(i: CommentNode) {
871 event.preventDefault();
873 if (i.state.banType == BanType.Community) {
874 let form: BanFromCommunityForm = {
875 user_id: i.props.node.comment.creator_id,
876 community_id: i.props.node.comment.community_id,
877 ban: !i.props.node.comment.banned_from_community,
878 reason: i.state.banReason,
879 expires: getUnixTime(i.state.banExpires),
881 WebSocketService.Instance.banFromCommunity(form);
883 let form: BanUserForm = {
884 user_id: i.props.node.comment.creator_id,
885 ban: !i.props.node.comment.banned,
886 reason: i.state.banReason,
887 expires: getUnixTime(i.state.banExpires),
889 WebSocketService.Instance.banUser(form);
892 i.state.showBanDialog = false;
896 handleShowConfirmAppointAsMod(i: CommentNode) {
897 i.state.showConfirmAppointAsMod = true;
901 handleCancelConfirmAppointAsMod(i: CommentNode) {
902 i.state.showConfirmAppointAsMod = false;
906 handleAddModToCommunity(i: CommentNode) {
907 let form: AddModToCommunityForm = {
908 user_id: i.props.node.comment.creator_id,
909 community_id: i.props.node.comment.community_id,
912 WebSocketService.Instance.addModToCommunity(form);
913 i.state.showConfirmAppointAsMod = false;
917 handleShowConfirmAppointAsAdmin(i: CommentNode) {
918 i.state.showConfirmAppointAsAdmin = true;
922 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
923 i.state.showConfirmAppointAsAdmin = false;
927 handleAddAdmin(i: CommentNode) {
928 let form: AddAdminForm = {
929 user_id: i.props.node.comment.creator_id,
932 WebSocketService.Instance.addAdmin(form);
933 i.state.showConfirmAppointAsAdmin = false;
937 handleShowConfirmTransferCommunity(i: CommentNode) {
938 i.state.showConfirmTransferCommunity = true;
942 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
943 i.state.showConfirmTransferCommunity = false;
947 handleTransferCommunity(i: CommentNode) {
948 let form: TransferCommunityForm = {
949 community_id: i.props.node.comment.community_id,
950 user_id: i.props.node.comment.creator_id,
952 WebSocketService.Instance.transferCommunity(form);
953 i.state.showConfirmTransferCommunity = false;
957 handleShowConfirmTransferSite(i: CommentNode) {
958 i.state.showConfirmTransferSite = true;
962 handleCancelShowConfirmTransferSite(i: CommentNode) {
963 i.state.showConfirmTransferSite = false;
967 handleTransferSite(i: CommentNode) {
968 let form: TransferSiteForm = {
969 user_id: i.props.node.comment.creator_id,
971 WebSocketService.Instance.transferSite(form);
972 i.state.showConfirmTransferSite = false;
976 get isCommentNew(): boolean {
977 let now = moment.utc().subtract(10, 'minutes');
978 let then = moment.utc(this.props.node.comment.published);
979 return now.isBefore(then);
982 handleCommentCollapse(i: CommentNode) {
983 i.state.collapsed = !i.state.collapsed;
987 handleViewSource(i: CommentNode) {
988 i.state.viewSource = !i.state.viewSource;