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 && (
128 <hr class="d-sm-none my-2" />
131 id={`comment-${node.comment.id}`}
132 className={`details comment-node mb-1 ${
133 this.isCommentNew ? 'mark' : ''
136 !this.props.noIndent &&
137 this.props.node.comment.parent_id &&
138 `border-left: 1px ${this.state.borderColor} solid !important`
142 class={`${!this.props.noIndent &&
143 this.props.node.comment.parent_id &&
146 <ul class="list-inline mb-1 text-muted small">
147 <li className="list-inline-item">
149 className="text-body font-weight-bold"
150 to={`/u/${node.comment.creator_name}`}
152 {node.comment.creator_avatar && showAvatars() && (
156 src={pictshareAvatarThumbnail(
157 node.comment.creator_avatar
159 class="rounded-circle mr-1"
162 <span>{node.comment.creator_name}</span>
166 <li className="list-inline-item badge badge-light">
171 <li className="list-inline-item badge badge-light">
175 {this.isPostCreator && (
176 <li className="list-inline-item badge badge-light">
180 {(node.comment.banned_from_community || node.comment.banned) && (
181 <li className="list-inline-item badge badge-danger">
185 <li className="list-inline-item">•</li>
187 class="unselectable pointer mr-2"
188 data-tippy-content={i18n.t('number_of_points', {
189 count: this.state.score,
192 <li className="list-inline-item">
193 <span className={this.scoreColor}>
194 <svg className="small icon icon-inline mr-1">
195 <use xlinkHref="#icon-zap"></use>
200 <li className="list-inline-item">
201 <svg class="small icon icon-inline mr-1">
202 <use xlinkHref="#icon-arrow-up"></use>
206 <li className="list-inline-item">
207 <svg class="small icon icon-inline mr-1">
208 <use xlinkHref="#icon-arrow-down"></use>
210 {this.state.downvotes}
213 {this.props.showCommunity && (
214 <li className="list-inline-item">
215 <span> {i18n.t('to')} </span>
216 <Link to={`/c/${node.comment.community_name}`}>
217 {node.comment.community_name}
221 <li className="list-inline-item">•</li>
222 <li className="list-inline-item">
224 <MomentTime data={node.comment} />
227 <li className="list-inline-item">
229 className="unselectable pointer text-monospace"
230 onClick={linkEvent(this, this.handleCommentCollapse)}
232 {this.state.collapsed ? (
234 <use xlinkHref="#icon-plus-square"></use>
238 <use xlinkHref="#icon-minus-square"></use>
244 {this.state.showEdit && (
248 onReplyCancel={this.handleReplyCancel}
249 disabled={this.props.locked}
252 {!this.state.showEdit && !this.state.collapsed && (
254 {this.state.viewSource ? (
255 <pre>{this.commentUnlessRemoved}</pre>
259 dangerouslySetInnerHTML={mdToHtml(
260 this.commentUnlessRemoved
264 <ul class="list-inline mb-0 text-muted font-weight-bold h5">
265 {this.props.markable && (
266 <li className="list-inline-item-action">
269 onClick={linkEvent(this, this.handleMarkRead)}
272 ? i18n.t('mark_as_unread')
273 : i18n.t('mark_as_read')
277 class={`icon icon-inline ${node.comment.read &&
280 <use xlinkHref="#icon-check"></use>
285 {UserService.Instance.user && !this.props.viewOnly && (
287 <li className="list-inline-item-action">
289 className={`vote-animate btn btn-link p-0 mb-1 ${
290 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
292 onClick={linkEvent(node, this.handleCommentUpvote)}
293 data-tippy-content={i18n.t('upvote')}
296 <use xlinkHref="#icon-arrow-up"></use>
300 {WebSocketService.Instance.site.enable_downvotes && (
301 <li className="list-inline-item-action">
303 className={`vote-animate btn btn-link p-0 mb-1 ${
304 this.state.my_vote == -1
310 this.handleCommentDownvote
312 data-tippy-content={i18n.t('downvote')}
315 <use xlinkHref="#icon-arrow-down"></use>
320 <li className="list-inline-item-action">
323 onClick={linkEvent(this, this.handleReplyClick)}
324 data-tippy-content={i18n.t('reply')}
326 <svg class="icon icon-inline">
327 <use xlinkHref="#icon-reply1"></use>
331 {!this.myComment && (
332 <li className="list-inline-item-action">
335 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
336 title={i18n.t('message').toLowerCase()}
339 <use xlinkHref="#icon-mail"></use>
344 <li className="list-inline-item-action">
346 className="text-muted"
347 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
348 title={i18n.t('link')}
350 <svg class="icon icon-inline">
351 <use xlinkHref="#icon-link"></use>
355 {!this.state.showAdvanced ? (
356 <li className="list-inline-item-action">
358 className="unselectable pointer"
359 onClick={linkEvent(this, this.handleShowAdvanced)}
360 data-tippy-content={i18n.t('more')}
362 <svg class="icon icon-inline">
363 <use xlinkHref="#icon-more-vertical"></use>
369 <li className="list-inline-item-action">
374 this.handleSaveCommentClick
383 class={`icon icon-inline ${node.comment.saved &&
386 <use xlinkHref="#icon-star"></use>
390 <li className="list-inline-item-action">
393 onClick={linkEvent(this, this.handleViewSource)}
394 data-tippy-content={i18n.t('view_source')}
397 class={`icon icon-inline ${this.state
398 .viewSource && 'text-success'}`}
400 <use xlinkHref="#icon-file-text"></use>
406 <li className="list-inline-item-action">•</li>
407 <li className="list-inline-item-action">
414 data-tippy-content={i18n.t('edit')}
416 <svg class="icon icon-inline">
417 <use xlinkHref="#icon-edit"></use>
421 <li className="list-inline-item-action">
426 this.handleDeleteClick
429 !node.comment.deleted
435 class={`icon icon-inline ${node.comment
436 .deleted && 'text-danger'}`}
438 <use xlinkHref="#icon-trash"></use>
444 {/* Admins and mods can remove comments */}
445 {(this.canMod || this.canAdmin) && (
447 <li className="list-inline-item-action">
448 {!node.comment.removed ? (
453 this.handleModRemoveShow
463 this.handleModRemoveSubmit
472 {/* Mods can ban from community, and appoint as mods to community */}
476 <li className="list-inline-item-action">
477 {!node.comment.banned_from_community ? (
482 this.handleModBanFromCommunityShow
492 this.handleModBanFromCommunitySubmit
500 {!node.comment.banned_from_community && (
501 <li className="list-inline-item-action">
502 {!this.state.showConfirmAppointAsMod ? (
507 this.handleShowConfirmAppointAsMod
511 ? i18n.t('remove_as_mod')
512 : i18n.t('appoint_as_mod')}
516 <span class="d-inline-block mr-1">
517 {i18n.t('are_you_sure')}
520 class="pointer d-inline-block mr-1"
523 this.handleAddModToCommunity
529 class="pointer d-inline-block"
532 this.handleCancelConfirmAppointAsMod
543 {/* Community creators and admins can transfer community to another mod */}
544 {(this.amCommunityCreator || this.canAdmin) &&
546 <li className="list-inline-item-action">
547 {!this.state.showConfirmTransferCommunity ? (
552 this.handleShowConfirmTransferCommunity
555 {i18n.t('transfer_community')}
559 <span class="d-inline-block mr-1">
560 {i18n.t('are_you_sure')}
563 class="pointer d-inline-block mr-1"
566 this.handleTransferCommunity
572 class="pointer d-inline-block"
576 .handleCancelShowConfirmTransferCommunity
585 {/* Admins can ban from all, and appoint other admins */}
589 <li className="list-inline-item-action">
590 {!node.comment.banned ? (
595 this.handleModBanShow
598 {i18n.t('ban_from_site')}
605 this.handleModBanSubmit
608 {i18n.t('unban_from_site')}
613 {!node.comment.banned && (
614 <li className="list-inline-item-action">
615 {!this.state.showConfirmAppointAsAdmin ? (
620 this.handleShowConfirmAppointAsAdmin
624 ? i18n.t('remove_as_admin')
625 : i18n.t('appoint_as_admin')}
629 <span class="d-inline-block mr-1">
630 {i18n.t('are_you_sure')}
633 class="pointer d-inline-block mr-1"
642 class="pointer d-inline-block"
645 this.handleCancelConfirmAppointAsAdmin
656 {/* Site Creator can transfer to another admin */}
657 {this.amSiteCreator && this.isAdmin && (
658 <li className="list-inline-item-action">
659 {!this.state.showConfirmTransferSite ? (
664 this.handleShowConfirmTransferSite
667 {i18n.t('transfer_site')}
671 <span class="d-inline-block mr-1">
672 {i18n.t('are_you_sure')}
675 class="pointer d-inline-block mr-1"
678 this.handleTransferSite
684 class="pointer d-inline-block"
687 this.handleCancelShowConfirmTransferSite
705 {/* end of details */}
706 {this.state.showRemoveDialog && (
709 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
713 class="form-control mr-2"
714 placeholder={i18n.t('reason')}
715 value={this.state.removeReason}
716 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
718 <button type="submit" class="btn btn-secondary">
719 {i18n.t('remove_comment')}
723 {this.state.showBanDialog && (
724 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
725 <div class="form-group row">
726 <label class="col-form-label">{i18n.t('reason')}</label>
729 class="form-control mr-2"
730 placeholder={i18n.t('reason')}
731 value={this.state.banReason}
732 onInput={linkEvent(this, this.handleModBanReasonChange)}
735 {/* TODO hold off on expires until later */}
736 {/* <div class="form-group row"> */}
737 {/* <label class="col-form-label">Expires</label> */}
738 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
740 <div class="form-group row">
741 <button type="submit" class="btn btn-secondary">
742 {i18n.t('ban')} {node.comment.creator_name}
747 {this.state.showReply && (
750 onReplyCancel={this.handleReplyCancel}
751 disabled={this.props.locked}
754 {node.children && !this.state.collapsed && (
756 nodes={node.children}
757 locked={this.props.locked}
758 moderators={this.props.moderators}
759 admins={this.props.admins}
760 postCreatorId={this.props.postCreatorId}
761 sort={this.props.sort}
762 sortType={this.props.sortType}
765 {/* A collapsed clearfix */}
766 {this.state.collapsed && <div class="row col-12"></div>}
771 get myComment(): boolean {
773 UserService.Instance.user &&
774 this.props.node.comment.creator_id == UserService.Instance.user.id
778 get isMod(): boolean {
780 this.props.moderators &&
782 this.props.moderators.map(m => m.user_id),
783 this.props.node.comment.creator_id
788 get isAdmin(): boolean {
792 this.props.admins.map(a => a.id),
793 this.props.node.comment.creator_id
798 get isPostCreator(): boolean {
799 return this.props.node.comment.creator_id == this.props.postCreatorId;
802 get canMod(): boolean {
803 if (this.props.admins && this.props.moderators) {
804 let adminsThenMods = this.props.admins
806 .concat(this.props.moderators.map(m => m.user_id));
809 UserService.Instance.user,
811 this.props.node.comment.creator_id
818 get canAdmin(): boolean {
822 UserService.Instance.user,
823 this.props.admins.map(a => a.id),
824 this.props.node.comment.creator_id
829 get amCommunityCreator(): boolean {
831 this.props.moderators &&
832 UserService.Instance.user &&
833 this.props.node.comment.creator_id != UserService.Instance.user.id &&
834 UserService.Instance.user.id == this.props.moderators[0].user_id
838 get amSiteCreator(): boolean {
841 UserService.Instance.user &&
842 this.props.node.comment.creator_id != UserService.Instance.user.id &&
843 UserService.Instance.user.id == this.props.admins[0].id
847 get commentUnlessRemoved(): string {
848 let node = this.props.node;
849 return node.comment.removed
850 ? `*${i18n.t('removed')}*`
851 : node.comment.deleted
852 ? `*${i18n.t('deleted')}*`
853 : node.comment.content;
856 handleReplyClick(i: CommentNode) {
857 i.state.showReply = true;
861 handleEditClick(i: CommentNode) {
862 i.state.showEdit = true;
866 handleDeleteClick(i: CommentNode) {
867 let deleteForm: CommentFormI = {
868 content: i.props.node.comment.content,
869 edit_id: i.props.node.comment.id,
870 creator_id: i.props.node.comment.creator_id,
871 post_id: i.props.node.comment.post_id,
872 parent_id: i.props.node.comment.parent_id,
873 deleted: !i.props.node.comment.deleted,
876 WebSocketService.Instance.editComment(deleteForm);
879 handleSaveCommentClick(i: CommentNode) {
881 i.props.node.comment.saved == undefined
883 : !i.props.node.comment.saved;
884 let form: SaveCommentForm = {
885 comment_id: i.props.node.comment.id,
889 WebSocketService.Instance.saveComment(form);
892 handleReplyCancel() {
893 this.state.showReply = false;
894 this.state.showEdit = false;
895 this.setState(this.state);
898 handleCommentUpvote(i: CommentNodeI) {
899 let new_vote = this.state.my_vote == 1 ? 0 : 1;
901 if (this.state.my_vote == 1) {
903 this.state.upvotes--;
904 } else if (this.state.my_vote == -1) {
905 this.state.downvotes--;
906 this.state.upvotes++;
907 this.state.score += 2;
909 this.state.upvotes++;
913 this.state.my_vote = new_vote;
915 let form: CommentLikeForm = {
916 comment_id: i.comment.id,
917 post_id: i.comment.post_id,
918 score: this.state.my_vote,
921 WebSocketService.Instance.likeComment(form);
922 this.setState(this.state);
925 handleCommentDownvote(i: CommentNodeI) {
926 let new_vote = this.state.my_vote == -1 ? 0 : -1;
928 if (this.state.my_vote == 1) {
929 this.state.score -= 2;
930 this.state.upvotes--;
931 this.state.downvotes++;
932 } else if (this.state.my_vote == -1) {
933 this.state.downvotes--;
936 this.state.downvotes++;
940 this.state.my_vote = new_vote;
942 let form: CommentLikeForm = {
943 comment_id: i.comment.id,
944 post_id: i.comment.post_id,
945 score: this.state.my_vote,
948 WebSocketService.Instance.likeComment(form);
949 this.setState(this.state);
952 handleModRemoveShow(i: CommentNode) {
953 i.state.showRemoveDialog = true;
957 handleModRemoveReasonChange(i: CommentNode, event: any) {
958 i.state.removeReason = event.target.value;
962 handleModRemoveSubmit(i: CommentNode) {
963 event.preventDefault();
964 let form: CommentFormI = {
965 content: i.props.node.comment.content,
966 edit_id: i.props.node.comment.id,
967 creator_id: i.props.node.comment.creator_id,
968 post_id: i.props.node.comment.post_id,
969 parent_id: i.props.node.comment.parent_id,
970 removed: !i.props.node.comment.removed,
971 reason: i.state.removeReason,
974 WebSocketService.Instance.editComment(form);
976 i.state.showRemoveDialog = false;
980 handleMarkRead(i: CommentNode) {
981 // if it has a user_mention_id field, then its a mention
982 if (i.props.node.comment.user_mention_id) {
983 let form: EditUserMentionForm = {
984 user_mention_id: i.props.node.comment.user_mention_id,
985 read: !i.props.node.comment.read,
987 WebSocketService.Instance.editUserMention(form);
989 let form: CommentFormI = {
990 content: i.props.node.comment.content,
991 edit_id: i.props.node.comment.id,
992 creator_id: i.props.node.comment.creator_id,
993 post_id: i.props.node.comment.post_id,
994 parent_id: i.props.node.comment.parent_id,
995 read: !i.props.node.comment.read,
998 WebSocketService.Instance.editComment(form);
1002 handleModBanFromCommunityShow(i: CommentNode) {
1003 i.state.showBanDialog = !i.state.showBanDialog;
1004 i.state.banType = BanType.Community;
1005 i.setState(i.state);
1008 handleModBanShow(i: CommentNode) {
1009 i.state.showBanDialog = !i.state.showBanDialog;
1010 i.state.banType = BanType.Site;
1011 i.setState(i.state);
1014 handleModBanReasonChange(i: CommentNode, event: any) {
1015 i.state.banReason = event.target.value;
1016 i.setState(i.state);
1019 handleModBanExpiresChange(i: CommentNode, event: any) {
1020 i.state.banExpires = event.target.value;
1021 i.setState(i.state);
1024 handleModBanFromCommunitySubmit(i: CommentNode) {
1025 i.state.banType = BanType.Community;
1026 i.setState(i.state);
1027 i.handleModBanBothSubmit(i);
1030 handleModBanSubmit(i: CommentNode) {
1031 i.state.banType = BanType.Site;
1032 i.setState(i.state);
1033 i.handleModBanBothSubmit(i);
1036 handleModBanBothSubmit(i: CommentNode) {
1037 event.preventDefault();
1039 if (i.state.banType == BanType.Community) {
1040 let form: BanFromCommunityForm = {
1041 user_id: i.props.node.comment.creator_id,
1042 community_id: i.props.node.comment.community_id,
1043 ban: !i.props.node.comment.banned_from_community,
1044 reason: i.state.banReason,
1045 expires: getUnixTime(i.state.banExpires),
1047 WebSocketService.Instance.banFromCommunity(form);
1049 let form: BanUserForm = {
1050 user_id: i.props.node.comment.creator_id,
1051 ban: !i.props.node.comment.banned,
1052 reason: i.state.banReason,
1053 expires: getUnixTime(i.state.banExpires),
1055 WebSocketService.Instance.banUser(form);
1058 i.state.showBanDialog = false;
1059 i.setState(i.state);
1062 handleShowConfirmAppointAsMod(i: CommentNode) {
1063 i.state.showConfirmAppointAsMod = true;
1064 i.setState(i.state);
1067 handleCancelConfirmAppointAsMod(i: CommentNode) {
1068 i.state.showConfirmAppointAsMod = false;
1069 i.setState(i.state);
1072 handleAddModToCommunity(i: CommentNode) {
1073 let form: AddModToCommunityForm = {
1074 user_id: i.props.node.comment.creator_id,
1075 community_id: i.props.node.comment.community_id,
1078 WebSocketService.Instance.addModToCommunity(form);
1079 i.state.showConfirmAppointAsMod = false;
1080 i.setState(i.state);
1083 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1084 i.state.showConfirmAppointAsAdmin = true;
1085 i.setState(i.state);
1088 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1089 i.state.showConfirmAppointAsAdmin = false;
1090 i.setState(i.state);
1093 handleAddAdmin(i: CommentNode) {
1094 let form: AddAdminForm = {
1095 user_id: i.props.node.comment.creator_id,
1098 WebSocketService.Instance.addAdmin(form);
1099 i.state.showConfirmAppointAsAdmin = false;
1100 i.setState(i.state);
1103 handleShowConfirmTransferCommunity(i: CommentNode) {
1104 i.state.showConfirmTransferCommunity = true;
1105 i.setState(i.state);
1108 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1109 i.state.showConfirmTransferCommunity = false;
1110 i.setState(i.state);
1113 handleTransferCommunity(i: CommentNode) {
1114 let form: TransferCommunityForm = {
1115 community_id: i.props.node.comment.community_id,
1116 user_id: i.props.node.comment.creator_id,
1118 WebSocketService.Instance.transferCommunity(form);
1119 i.state.showConfirmTransferCommunity = false;
1120 i.setState(i.state);
1123 handleShowConfirmTransferSite(i: CommentNode) {
1124 i.state.showConfirmTransferSite = true;
1125 i.setState(i.state);
1128 handleCancelShowConfirmTransferSite(i: CommentNode) {
1129 i.state.showConfirmTransferSite = false;
1130 i.setState(i.state);
1133 handleTransferSite(i: CommentNode) {
1134 let form: TransferSiteForm = {
1135 user_id: i.props.node.comment.creator_id,
1137 WebSocketService.Instance.transferSite(form);
1138 i.state.showConfirmTransferSite = false;
1139 i.setState(i.state);
1142 get isCommentNew(): boolean {
1143 let now = moment.utc().subtract(10, 'minutes');
1144 let then = moment.utc(this.props.node.comment.published);
1145 return now.isBefore(then);
1148 handleCommentCollapse(i: CommentNode) {
1149 i.state.collapsed = !i.state.collapsed;
1150 i.setState(i.state);
1153 handleViewSource(i: CommentNode) {
1154 i.state.viewSource = !i.state.viewSource;
1155 i.setState(i.state);
1158 handleShowAdvanced(i: CommentNode) {
1159 i.state.showAdvanced = !i.state.showAdvanced;
1160 i.setState(i.state);
1165 if (this.state.my_vote == 1) {
1167 } else if (this.state.my_vote == -1) {
1168 return 'text-danger';
1170 return 'text-muted';