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={i18n.t('number_of_points', {
202 count: this.state.score,
205 <svg class="icon icon-inline mr-1">
206 <use xlinkHref="#icon-zap"></use>
211 <li className="list-inline-item">•</li>
212 <li className="list-inline-item">
214 <MomentTime data={node.comment} />
217 <li className="list-inline-item">
219 className="unselectable pointer text-monospace"
220 onClick={linkEvent(this, this.handleCommentCollapse)}
222 {this.state.collapsed ? (
223 <svg class="icon icon-inline">
224 <use xlinkHref="#icon-plus-square"></use>
227 <svg class="icon icon-inline">
228 <use xlinkHref="#icon-minus-square"></use>
234 {this.state.showEdit && (
238 onReplyCancel={this.handleReplyCancel}
239 disabled={this.props.locked}
242 {!this.state.showEdit && !this.state.collapsed && (
244 {this.state.viewSource ? (
245 <pre>{this.commentUnlessRemoved}</pre>
249 dangerouslySetInnerHTML={mdToHtml(
250 this.commentUnlessRemoved
254 <ul class="list-inline mb-0 text-muted font-weight-bold h5">
255 {this.props.markable && (
256 <li className="list-inline-item-action">
259 onClick={linkEvent(this, this.handleMarkRead)}
262 ? i18n.t('mark_as_unread')
263 : i18n.t('mark_as_read')
267 class={`icon icon-inline ${node.comment.read &&
270 <use xlinkHref="#icon-check"></use>
275 {UserService.Instance.user && !this.props.viewOnly && (
277 <li className="list-inline-item-action">
279 className={`vote-animate btn btn-link p-0 mb-1 ${
280 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
282 onClick={linkEvent(node, this.handleCommentUpvote)}
283 data-tippy-content={i18n.t('upvote')}
285 <svg class="icon icon-inline">
286 <use xlinkHref="#icon-arrow-up"></use>
288 {this.state.upvotes !== this.state.score && (
289 <span class="ml-1">{this.state.upvotes}</span>
293 {WebSocketService.Instance.site.enable_downvotes && (
294 <li className="list-inline-item-action">
296 className={`vote-animate btn btn-link p-0 mb-1 ${
297 this.state.my_vote == -1
303 this.handleCommentDownvote
305 data-tippy-content={i18n.t('downvote')}
307 <svg class="icon icon-inline">
308 <use xlinkHref="#icon-arrow-down"></use>
310 {this.state.upvotes !== this.state.score && (
311 <span class="ml-1">{this.state.downvotes}</span>
316 <li className="list-inline-item-action">
319 onClick={linkEvent(this, this.handleReplyClick)}
320 data-tippy-content={i18n.t('reply')}
322 <svg class="icon icon-inline">
323 <use xlinkHref="#icon-reply1"></use>
327 <li className="list-inline-item-action">
329 className="text-muted"
330 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
331 title={i18n.t('link')}
333 <svg class="icon icon-inline">
334 <use xlinkHref="#icon-link"></use>
338 {!this.state.showAdvanced ? (
339 <li className="list-inline-item-action">
341 className="unselectable pointer"
342 onClick={linkEvent(this, this.handleShowAdvanced)}
343 data-tippy-content={i18n.t('more')}
345 <svg class="icon icon-inline">
346 <use xlinkHref="#icon-more-vertical"></use>
352 {!this.myComment && (
353 <li className="list-inline-item-action">
356 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
357 title={i18n.t('message').toLowerCase()}
360 <use xlinkHref="#icon-mail"></use>
365 <li className="list-inline-item-action">
370 this.handleSaveCommentClick
379 class={`icon icon-inline ${node.comment.saved &&
382 <use xlinkHref="#icon-star"></use>
386 <li className="list-inline-item-action">
389 onClick={linkEvent(this, this.handleViewSource)}
390 data-tippy-content={i18n.t('view_source')}
393 class={`icon icon-inline ${this.state
394 .viewSource && 'text-success'}`}
396 <use xlinkHref="#icon-file-text"></use>
402 <li className="list-inline-item-action">•</li>
403 <li className="list-inline-item-action">
410 data-tippy-content={i18n.t('edit')}
412 <svg class="icon icon-inline">
413 <use xlinkHref="#icon-edit"></use>
417 <li className="list-inline-item-action">
422 this.handleDeleteClick
425 !node.comment.deleted
431 class={`icon icon-inline ${node.comment
432 .deleted && 'text-danger'}`}
434 <use xlinkHref="#icon-trash"></use>
440 {/* Admins and mods can remove comments */}
441 {(this.canMod || this.canAdmin) && (
443 <li className="list-inline-item-action">
444 {!node.comment.removed ? (
449 this.handleModRemoveShow
459 this.handleModRemoveSubmit
468 {/* Mods can ban from community, and appoint as mods to community */}
472 <li className="list-inline-item-action">
473 {!node.comment.banned_from_community ? (
478 this.handleModBanFromCommunityShow
488 this.handleModBanFromCommunitySubmit
496 {!node.comment.banned_from_community && (
497 <li className="list-inline-item-action">
498 {!this.state.showConfirmAppointAsMod ? (
503 this.handleShowConfirmAppointAsMod
507 ? i18n.t('remove_as_mod')
508 : i18n.t('appoint_as_mod')}
512 <span class="d-inline-block mr-1">
513 {i18n.t('are_you_sure')}
516 class="pointer d-inline-block mr-1"
519 this.handleAddModToCommunity
525 class="pointer d-inline-block"
528 this.handleCancelConfirmAppointAsMod
539 {/* Community creators and admins can transfer community to another mod */}
540 {(this.amCommunityCreator || this.canAdmin) &&
542 <li className="list-inline-item-action">
543 {!this.state.showConfirmTransferCommunity ? (
548 this.handleShowConfirmTransferCommunity
551 {i18n.t('transfer_community')}
555 <span class="d-inline-block mr-1">
556 {i18n.t('are_you_sure')}
559 class="pointer d-inline-block mr-1"
562 this.handleTransferCommunity
568 class="pointer d-inline-block"
572 .handleCancelShowConfirmTransferCommunity
581 {/* Admins can ban from all, and appoint other admins */}
585 <li className="list-inline-item-action">
586 {!node.comment.banned ? (
591 this.handleModBanShow
594 {i18n.t('ban_from_site')}
601 this.handleModBanSubmit
604 {i18n.t('unban_from_site')}
609 {!node.comment.banned && (
610 <li className="list-inline-item-action">
611 {!this.state.showConfirmAppointAsAdmin ? (
616 this.handleShowConfirmAppointAsAdmin
620 ? i18n.t('remove_as_admin')
621 : i18n.t('appoint_as_admin')}
625 <span class="d-inline-block mr-1">
626 {i18n.t('are_you_sure')}
629 class="pointer d-inline-block mr-1"
638 class="pointer d-inline-block"
641 this.handleCancelConfirmAppointAsAdmin
652 {/* Site Creator can transfer to another admin */}
653 {this.amSiteCreator && this.isAdmin && (
654 <li className="list-inline-item-action">
655 {!this.state.showConfirmTransferSite ? (
660 this.handleShowConfirmTransferSite
663 {i18n.t('transfer_site')}
667 <span class="d-inline-block mr-1">
668 {i18n.t('are_you_sure')}
671 class="pointer d-inline-block mr-1"
674 this.handleTransferSite
680 class="pointer d-inline-block"
683 this.handleCancelShowConfirmTransferSite
701 {/* end of details */}
702 {this.state.showRemoveDialog && (
705 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
709 class="form-control mr-2"
710 placeholder={i18n.t('reason')}
711 value={this.state.removeReason}
712 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
714 <button type="submit" class="btn btn-secondary">
715 {i18n.t('remove_comment')}
719 {this.state.showBanDialog && (
720 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
721 <div class="form-group row">
722 <label class="col-form-label">{i18n.t('reason')}</label>
725 class="form-control mr-2"
726 placeholder={i18n.t('reason')}
727 value={this.state.banReason}
728 onInput={linkEvent(this, this.handleModBanReasonChange)}
731 {/* TODO hold off on expires until later */}
732 {/* <div class="form-group row"> */}
733 {/* <label class="col-form-label">Expires</label> */}
734 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
736 <div class="form-group row">
737 <button type="submit" class="btn btn-secondary">
738 {i18n.t('ban')} {node.comment.creator_name}
743 {this.state.showReply && (
746 onReplyCancel={this.handleReplyCancel}
747 disabled={this.props.locked}
750 {node.children && !this.state.collapsed && (
752 nodes={node.children}
753 locked={this.props.locked}
754 moderators={this.props.moderators}
755 admins={this.props.admins}
756 postCreatorId={this.props.postCreatorId}
757 sort={this.props.sort}
758 sortType={this.props.sortType}
761 {/* A collapsed clearfix */}
762 {this.state.collapsed && <div class="row col-12"></div>}
767 get myComment(): boolean {
769 UserService.Instance.user &&
770 this.props.node.comment.creator_id == UserService.Instance.user.id
774 get isMod(): boolean {
776 this.props.moderators &&
778 this.props.moderators.map(m => m.user_id),
779 this.props.node.comment.creator_id
784 get isAdmin(): boolean {
788 this.props.admins.map(a => a.id),
789 this.props.node.comment.creator_id
794 get isPostCreator(): boolean {
795 return this.props.node.comment.creator_id == this.props.postCreatorId;
798 get canMod(): boolean {
799 if (this.props.admins && this.props.moderators) {
800 let adminsThenMods = this.props.admins
802 .concat(this.props.moderators.map(m => m.user_id));
805 UserService.Instance.user,
807 this.props.node.comment.creator_id
814 get canAdmin(): boolean {
818 UserService.Instance.user,
819 this.props.admins.map(a => a.id),
820 this.props.node.comment.creator_id
825 get amCommunityCreator(): boolean {
827 this.props.moderators &&
828 UserService.Instance.user &&
829 this.props.node.comment.creator_id != UserService.Instance.user.id &&
830 UserService.Instance.user.id == this.props.moderators[0].user_id
834 get amSiteCreator(): boolean {
837 UserService.Instance.user &&
838 this.props.node.comment.creator_id != UserService.Instance.user.id &&
839 UserService.Instance.user.id == this.props.admins[0].id
843 get commentUnlessRemoved(): string {
844 let node = this.props.node;
845 return node.comment.removed
846 ? `*${i18n.t('removed')}*`
847 : node.comment.deleted
848 ? `*${i18n.t('deleted')}*`
849 : node.comment.content;
852 handleReplyClick(i: CommentNode) {
853 i.state.showReply = true;
857 handleEditClick(i: CommentNode) {
858 i.state.showEdit = true;
862 handleDeleteClick(i: CommentNode) {
863 let deleteForm: CommentFormI = {
864 content: i.props.node.comment.content,
865 edit_id: i.props.node.comment.id,
866 creator_id: i.props.node.comment.creator_id,
867 post_id: i.props.node.comment.post_id,
868 parent_id: i.props.node.comment.parent_id,
869 deleted: !i.props.node.comment.deleted,
872 WebSocketService.Instance.editComment(deleteForm);
875 handleSaveCommentClick(i: CommentNode) {
877 i.props.node.comment.saved == undefined
879 : !i.props.node.comment.saved;
880 let form: SaveCommentForm = {
881 comment_id: i.props.node.comment.id,
885 WebSocketService.Instance.saveComment(form);
888 handleReplyCancel() {
889 this.state.showReply = false;
890 this.state.showEdit = false;
891 this.setState(this.state);
894 handleCommentUpvote(i: CommentNodeI) {
895 let new_vote = this.state.my_vote == 1 ? 0 : 1;
897 if (this.state.my_vote == 1) {
899 this.state.upvotes--;
900 } else if (this.state.my_vote == -1) {
901 this.state.downvotes--;
902 this.state.upvotes++;
903 this.state.score += 2;
905 this.state.upvotes++;
909 this.state.my_vote = new_vote;
911 let form: CommentLikeForm = {
912 comment_id: i.comment.id,
913 post_id: i.comment.post_id,
914 score: this.state.my_vote,
917 WebSocketService.Instance.likeComment(form);
918 this.setState(this.state);
921 handleCommentDownvote(i: CommentNodeI) {
922 let new_vote = this.state.my_vote == -1 ? 0 : -1;
924 if (this.state.my_vote == 1) {
925 this.state.score -= 2;
926 this.state.upvotes--;
927 this.state.downvotes++;
928 } else if (this.state.my_vote == -1) {
929 this.state.downvotes--;
932 this.state.downvotes++;
936 this.state.my_vote = new_vote;
938 let form: CommentLikeForm = {
939 comment_id: i.comment.id,
940 post_id: i.comment.post_id,
941 score: this.state.my_vote,
944 WebSocketService.Instance.likeComment(form);
945 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';