1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
4 CommentNode as CommentNodeI,
6 CommentForm as CommentFormI,
13 AddModToCommunityForm,
15 TransferCommunityForm,
20 } from '../interfaces';
21 import { WebSocketService, UserService } from '../services';
27 pictshareAvatarThumbnail,
32 import moment from 'moment';
33 import { MomentTime } from './moment-time';
34 import { CommentForm } from './comment-form';
35 import { CommentNodes } from './comment-nodes';
36 import { i18n } from '../i18next';
38 interface CommentNodeState {
41 showRemoveDialog: boolean;
43 showBanDialog: boolean;
47 showConfirmTransferSite: boolean;
48 showConfirmTransferCommunity: boolean;
49 showConfirmAppointAsMod: boolean;
50 showConfirmAppointAsAdmin: boolean;
53 showAdvanced: boolean;
61 interface CommentNodeProps {
67 moderators: Array<CommunityUser>;
68 admins: Array<UserView>;
69 // TODO is this necessary, can't I get it from the node itself?
70 postCreatorId?: number;
71 showCommunity?: boolean;
72 sort?: CommentSortType;
76 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
77 private emptyState: CommentNodeState = {
80 showRemoveDialog: false,
85 banType: BanType.Community,
89 showConfirmTransferSite: false,
90 showConfirmTransferCommunity: false,
91 showConfirmAppointAsMod: false,
92 showConfirmAppointAsAdmin: false,
93 my_vote: this.props.node.comment.my_vote,
94 score: this.props.node.comment.score,
95 upvotes: this.props.node.comment.upvotes,
96 downvotes: this.props.node.comment.downvotes,
97 borderColor: this.props.node.comment.depth
98 ? colorList[this.props.node.comment.depth % colorList.length]
102 constructor(props: any, context: any) {
103 super(props, context);
105 this.state = this.emptyState;
106 this.handleReplyCancel = this.handleReplyCancel.bind(this);
107 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
108 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
111 componentWillReceiveProps(nextProps: CommentNodeProps) {
112 this.state.my_vote = nextProps.node.comment.my_vote;
113 this.state.upvotes = nextProps.node.comment.upvotes;
114 this.state.downvotes = nextProps.node.comment.downvotes;
115 this.state.score = nextProps.node.comment.score;
116 this.setState(this.state);
120 let node = this.props.node;
123 className={`comment ${
124 node.comment.parent_id && !this.props.noIndent ? 'ml-2' : ''
127 {!node.comment.parent_id && !this.props.noIndent && (
129 <hr class="d-sm-none my-2" />
130 <div class="d-none d-sm-block d-sm-none my-3" />
134 id={`comment-${node.comment.id}`}
135 className={`details comment-node mb-1 ${
136 this.isCommentNew ? 'mark' : ''
139 !this.props.noIndent &&
140 this.props.node.comment.parent_id &&
141 `border-left: 2px ${this.state.borderColor} solid !important`
145 class={`${!this.props.noIndent &&
146 this.props.node.comment.parent_id &&
149 <ul class="list-inline mb-1 text-muted small">
150 <li className="list-inline-item">
152 className="text-body font-weight-bold"
153 to={`/u/${node.comment.creator_name}`}
155 {node.comment.creator_avatar && showAvatars() && (
159 src={pictshareAvatarThumbnail(
160 node.comment.creator_avatar
162 class="rounded-circle mr-1"
165 <span>{node.comment.creator_name}</span>
169 <li className="list-inline-item badge badge-light">
174 <li className="list-inline-item badge badge-light">
178 {this.isPostCreator && (
179 <li className="list-inline-item badge badge-light">
183 {(node.comment.banned_from_community || node.comment.banned) && (
184 <li className="list-inline-item badge badge-danger">
188 {this.props.showCommunity && (
189 <li className="list-inline-item">
190 <span> {i18n.t('to')} </span>
191 <Link to={`/c/${node.comment.community_name}`}>
192 {node.comment.community_name}
196 <li className="list-inline-item">•</li>
197 <li className="list-inline-item">
199 className={`unselectable pointer ${this.scoreColor}`}
200 onClick={linkEvent(node, this.handleCommentUpvote)}
201 data-tippy-content={this.pointsTippy}
203 <svg class="icon icon-inline mr-1">
204 <use xlinkHref="#icon-zap"></use>
209 <li className="list-inline-item">•</li>
210 <li className="list-inline-item">
212 <MomentTime data={node.comment} />
215 <li className="list-inline-item">
217 className="unselectable pointer text-monospace"
218 onClick={linkEvent(this, this.handleCommentCollapse)}
220 {this.state.collapsed ? (
221 <svg class="icon icon-inline">
222 <use xlinkHref="#icon-plus-square"></use>
225 <svg class="icon icon-inline">
226 <use xlinkHref="#icon-minus-square"></use>
232 {this.state.showEdit && (
236 onReplyCancel={this.handleReplyCancel}
237 disabled={this.props.locked}
240 {!this.state.showEdit && !this.state.collapsed && (
242 {this.state.viewSource ? (
243 <pre>{this.commentUnlessRemoved}</pre>
247 dangerouslySetInnerHTML={mdToHtml(
248 this.commentUnlessRemoved
252 <ul class="list-inline mb-0 text-muted font-weight-bold h5">
253 {this.props.markable && (
254 <li className="list-inline-item-action">
257 onClick={linkEvent(this, this.handleMarkRead)}
260 ? i18n.t('mark_as_unread')
261 : i18n.t('mark_as_read')
265 class={`icon icon-inline ${node.comment.read &&
268 <use xlinkHref="#icon-check"></use>
273 {UserService.Instance.user && !this.props.viewOnly && (
275 <li className="list-inline-item-action">
277 className={`vote-animate btn btn-link p-0 mb-1 ${
278 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
280 onClick={linkEvent(node, this.handleCommentUpvote)}
281 data-tippy-content={i18n.t('upvote')}
283 <svg class="icon icon-inline">
284 <use xlinkHref="#icon-arrow-up"></use>
286 {this.state.upvotes !== this.state.score && (
287 <span class="ml-1">{this.state.upvotes}</span>
291 {WebSocketService.Instance.site.enable_downvotes && (
292 <li className="list-inline-item-action">
294 className={`vote-animate btn btn-link p-0 mb-1 ${
295 this.state.my_vote == -1
301 this.handleCommentDownvote
303 data-tippy-content={i18n.t('downvote')}
305 <svg class="icon icon-inline">
306 <use xlinkHref="#icon-arrow-down"></use>
308 {this.state.upvotes !== this.state.score && (
309 <span class="ml-1">{this.state.downvotes}</span>
314 <li className="list-inline-item-action">
317 onClick={linkEvent(this, this.handleReplyClick)}
318 data-tippy-content={i18n.t('reply')}
320 <svg class="icon icon-inline">
321 <use xlinkHref="#icon-reply1"></use>
325 <li className="list-inline-item-action">
327 className="text-muted"
328 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
329 title={i18n.t('link')}
331 <svg class="icon icon-inline">
332 <use xlinkHref="#icon-link"></use>
336 {!this.state.showAdvanced ? (
337 <li className="list-inline-item-action">
339 className="unselectable pointer"
340 onClick={linkEvent(this, this.handleShowAdvanced)}
341 data-tippy-content={i18n.t('more')}
343 <svg class="icon icon-inline">
344 <use xlinkHref="#icon-more-vertical"></use>
350 {!this.myComment && (
351 <li className="list-inline-item-action">
354 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
355 title={i18n.t('message').toLowerCase()}
358 <use xlinkHref="#icon-mail"></use>
363 <li className="list-inline-item-action">
368 this.handleSaveCommentClick
377 class={`icon icon-inline ${node.comment.saved &&
380 <use xlinkHref="#icon-star"></use>
384 <li className="list-inline-item-action">
387 onClick={linkEvent(this, this.handleViewSource)}
388 data-tippy-content={i18n.t('view_source')}
391 class={`icon icon-inline ${this.state
392 .viewSource && 'text-success'}`}
394 <use xlinkHref="#icon-file-text"></use>
400 <li className="list-inline-item-action">•</li>
401 <li className="list-inline-item-action">
408 data-tippy-content={i18n.t('edit')}
410 <svg class="icon icon-inline">
411 <use xlinkHref="#icon-edit"></use>
415 <li className="list-inline-item-action">
420 this.handleDeleteClick
423 !node.comment.deleted
429 class={`icon icon-inline ${node.comment
430 .deleted && 'text-danger'}`}
432 <use xlinkHref="#icon-trash"></use>
438 {/* Admins and mods can remove comments */}
439 {(this.canMod || this.canAdmin) && (
441 <li className="list-inline-item-action">
442 {!node.comment.removed ? (
447 this.handleModRemoveShow
457 this.handleModRemoveSubmit
466 {/* Mods can ban from community, and appoint as mods to community */}
470 <li className="list-inline-item-action">
471 {!node.comment.banned_from_community ? (
476 this.handleModBanFromCommunityShow
486 this.handleModBanFromCommunitySubmit
494 {!node.comment.banned_from_community && (
495 <li className="list-inline-item-action">
496 {!this.state.showConfirmAppointAsMod ? (
501 this.handleShowConfirmAppointAsMod
505 ? i18n.t('remove_as_mod')
506 : i18n.t('appoint_as_mod')}
510 <span class="d-inline-block mr-1">
511 {i18n.t('are_you_sure')}
514 class="pointer d-inline-block mr-1"
517 this.handleAddModToCommunity
523 class="pointer d-inline-block"
526 this.handleCancelConfirmAppointAsMod
537 {/* Community creators and admins can transfer community to another mod */}
538 {(this.amCommunityCreator || this.canAdmin) &&
540 <li className="list-inline-item-action">
541 {!this.state.showConfirmTransferCommunity ? (
546 this.handleShowConfirmTransferCommunity
549 {i18n.t('transfer_community')}
553 <span class="d-inline-block mr-1">
554 {i18n.t('are_you_sure')}
557 class="pointer d-inline-block mr-1"
560 this.handleTransferCommunity
566 class="pointer d-inline-block"
570 .handleCancelShowConfirmTransferCommunity
579 {/* Admins can ban from all, and appoint other admins */}
583 <li className="list-inline-item-action">
584 {!node.comment.banned ? (
589 this.handleModBanShow
592 {i18n.t('ban_from_site')}
599 this.handleModBanSubmit
602 {i18n.t('unban_from_site')}
607 {!node.comment.banned && (
608 <li className="list-inline-item-action">
609 {!this.state.showConfirmAppointAsAdmin ? (
614 this.handleShowConfirmAppointAsAdmin
618 ? i18n.t('remove_as_admin')
619 : i18n.t('appoint_as_admin')}
623 <span class="d-inline-block mr-1">
624 {i18n.t('are_you_sure')}
627 class="pointer d-inline-block mr-1"
636 class="pointer d-inline-block"
639 this.handleCancelConfirmAppointAsAdmin
650 {/* Site Creator can transfer to another admin */}
651 {this.amSiteCreator && this.isAdmin && (
652 <li className="list-inline-item-action">
653 {!this.state.showConfirmTransferSite ? (
658 this.handleShowConfirmTransferSite
661 {i18n.t('transfer_site')}
665 <span class="d-inline-block mr-1">
666 {i18n.t('are_you_sure')}
669 class="pointer d-inline-block mr-1"
672 this.handleTransferSite
678 class="pointer d-inline-block"
681 this.handleCancelShowConfirmTransferSite
699 {/* end of details */}
700 {this.state.showRemoveDialog && (
703 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
707 class="form-control mr-2"
708 placeholder={i18n.t('reason')}
709 value={this.state.removeReason}
710 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
712 <button type="submit" class="btn btn-secondary">
713 {i18n.t('remove_comment')}
717 {this.state.showBanDialog && (
718 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
719 <div class="form-group row">
720 <label class="col-form-label">{i18n.t('reason')}</label>
723 class="form-control mr-2"
724 placeholder={i18n.t('reason')}
725 value={this.state.banReason}
726 onInput={linkEvent(this, this.handleModBanReasonChange)}
729 {/* TODO hold off on expires until later */}
730 {/* <div class="form-group row"> */}
731 {/* <label class="col-form-label">Expires</label> */}
732 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
734 <div class="form-group row">
735 <button type="submit" class="btn btn-secondary">
736 {i18n.t('ban')} {node.comment.creator_name}
741 {this.state.showReply && (
744 onReplyCancel={this.handleReplyCancel}
745 disabled={this.props.locked}
748 {node.children && !this.state.collapsed && (
750 nodes={node.children}
751 locked={this.props.locked}
752 moderators={this.props.moderators}
753 admins={this.props.admins}
754 postCreatorId={this.props.postCreatorId}
755 sort={this.props.sort}
756 sortType={this.props.sortType}
759 {/* A collapsed clearfix */}
760 {this.state.collapsed && <div class="row col-12"></div>}
765 get myComment(): boolean {
767 UserService.Instance.user &&
768 this.props.node.comment.creator_id == UserService.Instance.user.id
772 get isMod(): boolean {
774 this.props.moderators &&
776 this.props.moderators.map(m => m.user_id),
777 this.props.node.comment.creator_id
782 get isAdmin(): boolean {
786 this.props.admins.map(a => a.id),
787 this.props.node.comment.creator_id
792 get isPostCreator(): boolean {
793 return this.props.node.comment.creator_id == this.props.postCreatorId;
796 get canMod(): boolean {
797 if (this.props.admins && this.props.moderators) {
798 let adminsThenMods = this.props.admins
800 .concat(this.props.moderators.map(m => m.user_id));
803 UserService.Instance.user,
805 this.props.node.comment.creator_id
812 get canAdmin(): boolean {
816 UserService.Instance.user,
817 this.props.admins.map(a => a.id),
818 this.props.node.comment.creator_id
823 get amCommunityCreator(): boolean {
825 this.props.moderators &&
826 UserService.Instance.user &&
827 this.props.node.comment.creator_id != UserService.Instance.user.id &&
828 UserService.Instance.user.id == this.props.moderators[0].user_id
832 get amSiteCreator(): boolean {
835 UserService.Instance.user &&
836 this.props.node.comment.creator_id != UserService.Instance.user.id &&
837 UserService.Instance.user.id == this.props.admins[0].id
841 get commentUnlessRemoved(): string {
842 let node = this.props.node;
843 return node.comment.removed
844 ? `*${i18n.t('removed')}*`
845 : node.comment.deleted
846 ? `*${i18n.t('deleted')}*`
847 : node.comment.content;
850 handleReplyClick(i: CommentNode) {
851 i.state.showReply = true;
855 handleEditClick(i: CommentNode) {
856 i.state.showEdit = true;
860 handleDeleteClick(i: CommentNode) {
861 let deleteForm: CommentFormI = {
862 content: i.props.node.comment.content,
863 edit_id: i.props.node.comment.id,
864 creator_id: i.props.node.comment.creator_id,
865 post_id: i.props.node.comment.post_id,
866 parent_id: i.props.node.comment.parent_id,
867 deleted: !i.props.node.comment.deleted,
870 WebSocketService.Instance.editComment(deleteForm);
873 handleSaveCommentClick(i: CommentNode) {
875 i.props.node.comment.saved == undefined
877 : !i.props.node.comment.saved;
878 let form: SaveCommentForm = {
879 comment_id: i.props.node.comment.id,
883 WebSocketService.Instance.saveComment(form);
886 handleReplyCancel() {
887 this.state.showReply = false;
888 this.state.showEdit = false;
889 this.setState(this.state);
892 handleCommentUpvote(i: CommentNodeI) {
893 let new_vote = this.state.my_vote == 1 ? 0 : 1;
895 if (this.state.my_vote == 1) {
897 this.state.upvotes--;
898 } else if (this.state.my_vote == -1) {
899 this.state.downvotes--;
900 this.state.upvotes++;
901 this.state.score += 2;
903 this.state.upvotes++;
907 this.state.my_vote = new_vote;
909 let form: CommentLikeForm = {
910 comment_id: i.comment.id,
911 post_id: i.comment.post_id,
912 score: this.state.my_vote,
915 WebSocketService.Instance.likeComment(form);
916 this.setState(this.state);
920 handleCommentDownvote(i: CommentNodeI) {
921 let new_vote = this.state.my_vote == -1 ? 0 : -1;
923 if (this.state.my_vote == 1) {
924 this.state.score -= 2;
925 this.state.upvotes--;
926 this.state.downvotes++;
927 } else if (this.state.my_vote == -1) {
928 this.state.downvotes--;
931 this.state.downvotes++;
935 this.state.my_vote = new_vote;
937 let form: CommentLikeForm = {
938 comment_id: i.comment.id,
939 post_id: i.comment.post_id,
940 score: this.state.my_vote,
943 WebSocketService.Instance.likeComment(form);
944 this.setState(this.state);
948 handleModRemoveShow(i: CommentNode) {
949 i.state.showRemoveDialog = true;
953 handleModRemoveReasonChange(i: CommentNode, event: any) {
954 i.state.removeReason = event.target.value;
958 handleModRemoveSubmit(i: CommentNode) {
959 event.preventDefault();
960 let form: CommentFormI = {
961 content: i.props.node.comment.content,
962 edit_id: i.props.node.comment.id,
963 creator_id: i.props.node.comment.creator_id,
964 post_id: i.props.node.comment.post_id,
965 parent_id: i.props.node.comment.parent_id,
966 removed: !i.props.node.comment.removed,
967 reason: i.state.removeReason,
970 WebSocketService.Instance.editComment(form);
972 i.state.showRemoveDialog = false;
976 handleMarkRead(i: CommentNode) {
977 // if it has a user_mention_id field, then its a mention
978 if (i.props.node.comment.user_mention_id) {
979 let form: EditUserMentionForm = {
980 user_mention_id: i.props.node.comment.user_mention_id,
981 read: !i.props.node.comment.read,
983 WebSocketService.Instance.editUserMention(form);
985 let form: CommentFormI = {
986 content: i.props.node.comment.content,
987 edit_id: i.props.node.comment.id,
988 creator_id: i.props.node.comment.creator_id,
989 post_id: i.props.node.comment.post_id,
990 parent_id: i.props.node.comment.parent_id,
991 read: !i.props.node.comment.read,
994 WebSocketService.Instance.editComment(form);
998 handleModBanFromCommunityShow(i: CommentNode) {
999 i.state.showBanDialog = !i.state.showBanDialog;
1000 i.state.banType = BanType.Community;
1001 i.setState(i.state);
1004 handleModBanShow(i: CommentNode) {
1005 i.state.showBanDialog = !i.state.showBanDialog;
1006 i.state.banType = BanType.Site;
1007 i.setState(i.state);
1010 handleModBanReasonChange(i: CommentNode, event: any) {
1011 i.state.banReason = event.target.value;
1012 i.setState(i.state);
1015 handleModBanExpiresChange(i: CommentNode, event: any) {
1016 i.state.banExpires = event.target.value;
1017 i.setState(i.state);
1020 handleModBanFromCommunitySubmit(i: CommentNode) {
1021 i.state.banType = BanType.Community;
1022 i.setState(i.state);
1023 i.handleModBanBothSubmit(i);
1026 handleModBanSubmit(i: CommentNode) {
1027 i.state.banType = BanType.Site;
1028 i.setState(i.state);
1029 i.handleModBanBothSubmit(i);
1032 handleModBanBothSubmit(i: CommentNode) {
1033 event.preventDefault();
1035 if (i.state.banType == BanType.Community) {
1036 let form: BanFromCommunityForm = {
1037 user_id: i.props.node.comment.creator_id,
1038 community_id: i.props.node.comment.community_id,
1039 ban: !i.props.node.comment.banned_from_community,
1040 reason: i.state.banReason,
1041 expires: getUnixTime(i.state.banExpires),
1043 WebSocketService.Instance.banFromCommunity(form);
1045 let form: BanUserForm = {
1046 user_id: i.props.node.comment.creator_id,
1047 ban: !i.props.node.comment.banned,
1048 reason: i.state.banReason,
1049 expires: getUnixTime(i.state.banExpires),
1051 WebSocketService.Instance.banUser(form);
1054 i.state.showBanDialog = false;
1055 i.setState(i.state);
1058 handleShowConfirmAppointAsMod(i: CommentNode) {
1059 i.state.showConfirmAppointAsMod = true;
1060 i.setState(i.state);
1063 handleCancelConfirmAppointAsMod(i: CommentNode) {
1064 i.state.showConfirmAppointAsMod = false;
1065 i.setState(i.state);
1068 handleAddModToCommunity(i: CommentNode) {
1069 let form: AddModToCommunityForm = {
1070 user_id: i.props.node.comment.creator_id,
1071 community_id: i.props.node.comment.community_id,
1074 WebSocketService.Instance.addModToCommunity(form);
1075 i.state.showConfirmAppointAsMod = false;
1076 i.setState(i.state);
1079 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1080 i.state.showConfirmAppointAsAdmin = true;
1081 i.setState(i.state);
1084 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1085 i.state.showConfirmAppointAsAdmin = false;
1086 i.setState(i.state);
1089 handleAddAdmin(i: CommentNode) {
1090 let form: AddAdminForm = {
1091 user_id: i.props.node.comment.creator_id,
1094 WebSocketService.Instance.addAdmin(form);
1095 i.state.showConfirmAppointAsAdmin = false;
1096 i.setState(i.state);
1099 handleShowConfirmTransferCommunity(i: CommentNode) {
1100 i.state.showConfirmTransferCommunity = true;
1101 i.setState(i.state);
1104 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1105 i.state.showConfirmTransferCommunity = false;
1106 i.setState(i.state);
1109 handleTransferCommunity(i: CommentNode) {
1110 let form: TransferCommunityForm = {
1111 community_id: i.props.node.comment.community_id,
1112 user_id: i.props.node.comment.creator_id,
1114 WebSocketService.Instance.transferCommunity(form);
1115 i.state.showConfirmTransferCommunity = false;
1116 i.setState(i.state);
1119 handleShowConfirmTransferSite(i: CommentNode) {
1120 i.state.showConfirmTransferSite = true;
1121 i.setState(i.state);
1124 handleCancelShowConfirmTransferSite(i: CommentNode) {
1125 i.state.showConfirmTransferSite = false;
1126 i.setState(i.state);
1129 handleTransferSite(i: CommentNode) {
1130 let form: TransferSiteForm = {
1131 user_id: i.props.node.comment.creator_id,
1133 WebSocketService.Instance.transferSite(form);
1134 i.state.showConfirmTransferSite = false;
1135 i.setState(i.state);
1138 get isCommentNew(): boolean {
1139 let now = moment.utc().subtract(10, 'minutes');
1140 let then = moment.utc(this.props.node.comment.published);
1141 return now.isBefore(then);
1144 handleCommentCollapse(i: CommentNode) {
1145 i.state.collapsed = !i.state.collapsed;
1146 i.setState(i.state);
1149 handleViewSource(i: CommentNode) {
1150 i.state.viewSource = !i.state.viewSource;
1151 i.setState(i.state);
1154 handleShowAdvanced(i: CommentNode) {
1155 i.state.showAdvanced = !i.state.showAdvanced;
1156 i.setState(i.state);
1161 if (this.state.my_vote == 1) {
1163 } else if (this.state.my_vote == -1) {
1164 return 'text-danger';
1166 return 'text-muted';
1170 get pointsTippy(): string {
1171 let points = i18n.t('number_of_points', {
1172 count: this.state.score,
1175 let upvotes = i18n.t('number_of_upvotes', {
1176 count: this.state.upvotes,
1179 let downvotes = i18n.t('number_of_downvotes', {
1180 count: this.state.downvotes,
1183 return `${points} • ${upvotes} • ${downvotes}`;