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,
31 import moment from 'moment';
32 import { MomentTime } from './moment-time';
33 import { CommentForm } from './comment-form';
34 import { CommentNodes } from './comment-nodes';
35 import { i18n } from '../i18next';
37 interface CommentNodeState {
40 showRemoveDialog: boolean;
42 showBanDialog: boolean;
46 showConfirmTransferSite: boolean;
47 showConfirmTransferCommunity: boolean;
48 showConfirmAppointAsMod: boolean;
49 showConfirmAppointAsAdmin: boolean;
52 showAdvanced: boolean;
59 interface CommentNodeProps {
65 moderators: Array<CommunityUser>;
66 admins: Array<UserView>;
67 // TODO is this necessary, can't I get it from the node itself?
68 postCreatorId?: number;
69 showCommunity?: boolean;
70 sort?: CommentSortType;
74 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
75 private emptyState: CommentNodeState = {
78 showRemoveDialog: false,
83 banType: BanType.Community,
87 showConfirmTransferSite: false,
88 showConfirmTransferCommunity: false,
89 showConfirmAppointAsMod: false,
90 showConfirmAppointAsAdmin: false,
91 my_vote: this.props.node.comment.my_vote,
92 score: this.props.node.comment.score,
93 upvotes: this.props.node.comment.upvotes,
94 downvotes: this.props.node.comment.downvotes,
97 constructor(props: any, context: any) {
98 super(props, context);
100 this.state = this.emptyState;
101 this.handleReplyCancel = this.handleReplyCancel.bind(this);
102 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
103 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
106 componentDidUpdate(prevProps: CommentNodeProps) {
107 let prevComment = prevProps.node.comment;
108 let comment = this.props.node.comment;
110 prevComment.saved !== comment.saved ||
111 prevComment.deleted !== comment.deleted ||
112 prevComment.read !== comment.read
118 componentWillReceiveProps(nextProps: CommentNodeProps) {
119 this.state.my_vote = nextProps.node.comment.my_vote;
120 this.state.upvotes = nextProps.node.comment.upvotes;
121 this.state.downvotes = nextProps.node.comment.downvotes;
122 this.state.score = nextProps.node.comment.score;
123 this.setState(this.state);
127 let node = this.props.node;
130 className={`comment ${
131 node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
135 id={`comment-${node.comment.id}`}
136 className={`details comment-node mb-1 ${
137 this.isCommentNew ? 'mark' : ''
140 <ul class="list-inline mb-1 text-muted small">
141 <li className="list-inline-item">
143 className="text-info"
144 to={`/u/${node.comment.creator_name}`}
146 {node.comment.creator_avatar && showAvatars() && (
150 src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
151 class="rounded-circle mr-1"
154 <span>{node.comment.creator_name}</span>
158 <li className="list-inline-item badge badge-light">
163 <li className="list-inline-item badge badge-light">
167 {this.isPostCreator && (
168 <li className="list-inline-item badge badge-light">
172 {(node.comment.banned_from_community || node.comment.banned) && (
173 <li className="list-inline-item badge badge-danger">
178 class="unselectable pointer mr-2"
179 data-tippy-content={i18n.t('number_of_points', {
180 count: this.state.score,
183 <li className="list-inline-item">
184 <span className="text-danger">
185 <svg class="small icon icon-inline mr-1">
186 <use xlinkHref="#icon-heart"></use>
191 <li className="list-inline-item">
192 <span className="text-info">
193 <svg class="small icon icon-inline mr-1">
194 <use xlinkHref="#icon-arrow-up"></use>
199 <li className="list-inline-item">
200 <span className="text-danger">
201 <svg class="small icon icon-inline mr-1">
202 <use xlinkHref="#icon-arrow-down"></use>
204 {this.state.downvotes}
208 {this.props.showCommunity && (
209 <li className="list-inline-item">
210 <span> {i18n.t('to')} </span>
211 <Link to={`/c/${node.comment.community_name}`}>
212 {node.comment.community_name}
216 <li className="list-inline-item">
218 <MomentTime data={node.comment} />
221 <li className="list-inline-item">
223 className="unselectable pointer text-monospace"
224 onClick={linkEvent(this, this.handleCommentCollapse)}
226 {this.state.collapsed ? (
228 <use xlinkHref="#icon-plus-square"></use>
232 <use xlinkHref="#icon-minus-square"></use>
238 {this.state.showEdit && (
242 onReplyCancel={this.handleReplyCancel}
243 disabled={this.props.locked}
246 {!this.state.showEdit && !this.state.collapsed && (
248 {this.state.viewSource ? (
249 <pre>{this.commentUnlessRemoved}</pre>
253 dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
256 <ul class="list-inline mb-0 text-muted font-weight-bold h5">
257 {this.props.markable && (
258 <li className="list-inline-item-action">
261 onClick={linkEvent(this, this.handleMarkRead)}
264 ? i18n.t('mark_as_unread')
265 : i18n.t('mark_as_read')
269 class={`icon icon-inline ${node.comment.read &&
272 <use xlinkHref="#icon-check"></use>
277 {UserService.Instance.user && !this.props.viewOnly && (
279 <li className="list-inline-item-action">
281 className={`vote-animate btn btn-link p-0 mb-1 ${
282 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
284 onClick={linkEvent(node, this.handleCommentUpvote)}
285 data-tippy-content={i18n.t('upvote')}
288 <use xlinkHref="#icon-arrow-up"></use>
292 {WebSocketService.Instance.site.enable_downvotes && (
293 <li className="list-inline-item-action">
295 className={`vote-animate btn btn-link p-0 mb-1 ${
296 this.state.my_vote == -1
300 onClick={linkEvent(node, this.handleCommentDownvote)}
301 data-tippy-content={i18n.t('downvote')}
304 <use xlinkHref="#icon-arrow-down"></use>
309 <li className="list-inline-item-action">
312 onClick={linkEvent(this, this.handleReplyClick)}
313 data-tippy-content={i18n.t('reply')}
315 <svg class="icon icon-inline">
316 <use xlinkHref="#icon-reply1"></use>
320 {!this.myComment && (
321 <li className="list-inline-item-action">
324 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
325 title={i18n.t('message').toLowerCase()}
328 <use xlinkHref="#icon-mail"></use>
333 <li className="list-inline-item-action">
335 className="text-muted"
336 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
337 title={i18n.t('link')}
339 <svg class="icon icon-inline">
340 <use xlinkHref="#icon-link"></use>
344 {!this.state.showAdvanced ? (
345 <li className="list-inline-item-action">
347 className="unselectable pointer"
348 onClick={linkEvent(this, this.handleShowAdvanced)}
349 data-tippy-content={i18n.t('more')}
351 <svg class="icon icon-inline">
352 <use xlinkHref="#icon-more-vertical"></use>
358 <li className="list-inline-item-action">
363 this.handleSaveCommentClick
372 class={`icon icon-inline ${node.comment.saved &&
375 <use xlinkHref="#icon-star"></use>
379 <li className="list-inline-item-action">
382 onClick={linkEvent(this, this.handleViewSource)}
383 data-tippy-content={i18n.t('view_source')}
386 class={`icon icon-inline ${this.state
387 .viewSource && 'text-success'}`}
389 <use xlinkHref="#icon-file-text"></use>
395 <li className="list-inline-item-action">•</li>
396 <li className="list-inline-item-action">
399 onClick={linkEvent(this, this.handleEditClick)}
400 data-tippy-content={i18n.t('edit')}
402 <svg class="icon icon-inline">
403 <use xlinkHref="#icon-edit"></use>
407 <li className="list-inline-item-action">
412 this.handleDeleteClick
415 !node.comment.deleted
421 class={`icon icon-inline ${node.comment
422 .deleted && 'text-danger'}`}
424 <use xlinkHref="#icon-trash"></use>
430 {/* Admins and mods can remove comments */}
431 {(this.canMod || this.canAdmin) && (
433 <li className="list-inline-item-action">
434 {!node.comment.removed ? (
439 this.handleModRemoveShow
449 this.handleModRemoveSubmit
458 {/* Mods can ban from community, and appoint as mods to community */}
462 <li className="list-inline-item-action">
463 {!node.comment.banned_from_community ? (
468 this.handleModBanFromCommunityShow
478 this.handleModBanFromCommunitySubmit
486 {!node.comment.banned_from_community && (
487 <li className="list-inline-item-action">
488 {!this.state.showConfirmAppointAsMod ? (
493 this.handleShowConfirmAppointAsMod
497 ? i18n.t('remove_as_mod')
498 : i18n.t('appoint_as_mod')}
502 <span class="d-inline-block mr-1">
503 {i18n.t('are_you_sure')}
506 class="pointer d-inline-block mr-1"
509 this.handleAddModToCommunity
515 class="pointer d-inline-block"
518 this.handleCancelConfirmAppointAsMod
529 {/* Community creators and admins can transfer community to another mod */}
530 {(this.amCommunityCreator || this.canAdmin) &&
532 <li className="list-inline-item-action">
533 {!this.state.showConfirmTransferCommunity ? (
538 this.handleShowConfirmTransferCommunity
541 {i18n.t('transfer_community')}
545 <span class="d-inline-block mr-1">
546 {i18n.t('are_you_sure')}
549 class="pointer d-inline-block mr-1"
552 this.handleTransferCommunity
558 class="pointer d-inline-block"
562 .handleCancelShowConfirmTransferCommunity
571 {/* Admins can ban from all, and appoint other admins */}
575 <li className="list-inline-item-action">
576 {!node.comment.banned ? (
581 this.handleModBanShow
584 {i18n.t('ban_from_site')}
591 this.handleModBanSubmit
594 {i18n.t('unban_from_site')}
599 {!node.comment.banned && (
600 <li className="list-inline-item-action">
601 {!this.state.showConfirmAppointAsAdmin ? (
606 this.handleShowConfirmAppointAsAdmin
610 ? i18n.t('remove_as_admin')
611 : i18n.t('appoint_as_admin')}
615 <span class="d-inline-block mr-1">
616 {i18n.t('are_you_sure')}
619 class="pointer d-inline-block mr-1"
628 class="pointer d-inline-block"
631 this.handleCancelConfirmAppointAsAdmin
642 {/* Site Creator can transfer to another admin */}
643 {this.amSiteCreator && this.isAdmin && (
644 <li className="list-inline-item-action">
645 {!this.state.showConfirmTransferSite ? (
650 this.handleShowConfirmTransferSite
653 {i18n.t('transfer_site')}
657 <span class="d-inline-block mr-1">
658 {i18n.t('are_you_sure')}
661 class="pointer d-inline-block mr-1"
664 this.handleTransferSite
670 class="pointer d-inline-block"
673 this.handleCancelShowConfirmTransferSite
690 {this.state.showRemoveDialog && (
693 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
697 class="form-control mr-2"
698 placeholder={i18n.t('reason')}
699 value={this.state.removeReason}
700 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
702 <button type="submit" class="btn btn-secondary">
703 {i18n.t('remove_comment')}
707 {this.state.showBanDialog && (
708 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
709 <div class="form-group row">
710 <label class="col-form-label">{i18n.t('reason')}</label>
713 class="form-control mr-2"
714 placeholder={i18n.t('reason')}
715 value={this.state.banReason}
716 onInput={linkEvent(this, this.handleModBanReasonChange)}
719 {/* TODO hold off on expires until later */}
720 {/* <div class="form-group row"> */}
721 {/* <label class="col-form-label">Expires</label> */}
722 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
724 <div class="form-group row">
725 <button type="submit" class="btn btn-secondary">
726 {i18n.t('ban')} {node.comment.creator_name}
731 {this.state.showReply && (
734 onReplyCancel={this.handleReplyCancel}
735 disabled={this.props.locked}
738 {node.children && !this.state.collapsed && (
740 nodes={node.children}
741 locked={this.props.locked}
742 moderators={this.props.moderators}
743 admins={this.props.admins}
744 postCreatorId={this.props.postCreatorId}
745 sort={this.props.sort}
746 sortType={this.props.sortType}
749 {/* A collapsed clearfix */}
750 {this.state.collapsed && <div class="row col-12"></div>}
755 get myComment(): boolean {
757 UserService.Instance.user &&
758 this.props.node.comment.creator_id == UserService.Instance.user.id
762 get isMod(): boolean {
764 this.props.moderators &&
766 this.props.moderators.map(m => m.user_id),
767 this.props.node.comment.creator_id
772 get isAdmin(): boolean {
776 this.props.admins.map(a => a.id),
777 this.props.node.comment.creator_id
782 get isPostCreator(): boolean {
783 return this.props.node.comment.creator_id == this.props.postCreatorId;
786 get canMod(): boolean {
787 if (this.props.admins && this.props.moderators) {
788 let adminsThenMods = this.props.admins
790 .concat(this.props.moderators.map(m => m.user_id));
793 UserService.Instance.user,
795 this.props.node.comment.creator_id
802 get canAdmin(): boolean {
806 UserService.Instance.user,
807 this.props.admins.map(a => a.id),
808 this.props.node.comment.creator_id
813 get amCommunityCreator(): boolean {
815 this.props.moderators &&
816 UserService.Instance.user &&
817 this.props.node.comment.creator_id != UserService.Instance.user.id &&
818 UserService.Instance.user.id == this.props.moderators[0].user_id
822 get amSiteCreator(): boolean {
825 UserService.Instance.user &&
826 this.props.node.comment.creator_id != UserService.Instance.user.id &&
827 UserService.Instance.user.id == this.props.admins[0].id
831 get commentUnlessRemoved(): string {
832 let node = this.props.node;
833 return node.comment.removed
834 ? `*${i18n.t('removed')}*`
835 : node.comment.deleted
836 ? `*${i18n.t('deleted')}*`
837 : node.comment.content;
840 handleReplyClick(i: CommentNode) {
841 i.state.showReply = true;
845 handleEditClick(i: CommentNode) {
846 i.state.showEdit = true;
850 handleDeleteClick(i: CommentNode) {
851 let deleteForm: CommentFormI = {
852 content: i.props.node.comment.content,
853 edit_id: i.props.node.comment.id,
854 creator_id: i.props.node.comment.creator_id,
855 post_id: i.props.node.comment.post_id,
856 parent_id: i.props.node.comment.parent_id,
857 deleted: !i.props.node.comment.deleted,
860 WebSocketService.Instance.editComment(deleteForm);
863 handleSaveCommentClick(i: CommentNode) {
865 i.props.node.comment.saved == undefined
867 : !i.props.node.comment.saved;
868 let form: SaveCommentForm = {
869 comment_id: i.props.node.comment.id,
873 WebSocketService.Instance.saveComment(form);
876 handleReplyCancel() {
877 this.state.showReply = false;
878 this.state.showEdit = false;
879 this.setState(this.state);
882 handleCommentUpvote(i: CommentNodeI) {
883 let new_vote = this.state.my_vote == 1 ? 0 : 1;
885 if (this.state.my_vote == 1) {
887 this.state.upvotes--;
888 } else if (this.state.my_vote == -1) {
889 this.state.downvotes--;
890 this.state.upvotes++;
891 this.state.score += 2;
893 this.state.upvotes++;
897 this.state.my_vote = new_vote;
899 let form: CommentLikeForm = {
900 comment_id: i.comment.id,
901 post_id: i.comment.post_id,
902 score: this.state.my_vote,
905 WebSocketService.Instance.likeComment(form);
906 this.setState(this.state);
909 handleCommentDownvote(i: CommentNodeI) {
910 let new_vote = this.state.my_vote == -1 ? 0 : -1;
912 if (this.state.my_vote == 1) {
913 this.state.score -= 2;
914 this.state.upvotes--;
915 this.state.downvotes++;
916 } else if (this.state.my_vote == -1) {
917 this.state.downvotes--;
920 this.state.downvotes++;
924 this.state.my_vote = new_vote;
926 let form: CommentLikeForm = {
927 comment_id: i.comment.id,
928 post_id: i.comment.post_id,
929 score: this.state.my_vote,
932 WebSocketService.Instance.likeComment(form);
933 this.setState(this.state);
936 handleModRemoveShow(i: CommentNode) {
937 i.state.showRemoveDialog = true;
941 handleModRemoveReasonChange(i: CommentNode, event: any) {
942 i.state.removeReason = event.target.value;
946 handleModRemoveSubmit(i: CommentNode) {
947 event.preventDefault();
948 let form: CommentFormI = {
949 content: i.props.node.comment.content,
950 edit_id: i.props.node.comment.id,
951 creator_id: i.props.node.comment.creator_id,
952 post_id: i.props.node.comment.post_id,
953 parent_id: i.props.node.comment.parent_id,
954 removed: !i.props.node.comment.removed,
955 reason: i.state.removeReason,
958 WebSocketService.Instance.editComment(form);
960 i.state.showRemoveDialog = false;
964 handleMarkRead(i: CommentNode) {
965 // if it has a user_mention_id field, then its a mention
966 if (i.props.node.comment.user_mention_id) {
967 let form: EditUserMentionForm = {
968 user_mention_id: i.props.node.comment.user_mention_id,
969 read: !i.props.node.comment.read,
971 WebSocketService.Instance.editUserMention(form);
973 let form: CommentFormI = {
974 content: i.props.node.comment.content,
975 edit_id: i.props.node.comment.id,
976 creator_id: i.props.node.comment.creator_id,
977 post_id: i.props.node.comment.post_id,
978 parent_id: i.props.node.comment.parent_id,
979 read: !i.props.node.comment.read,
982 WebSocketService.Instance.editComment(form);
986 handleModBanFromCommunityShow(i: CommentNode) {
987 i.state.showBanDialog = !i.state.showBanDialog;
988 i.state.banType = BanType.Community;
992 handleModBanShow(i: CommentNode) {
993 i.state.showBanDialog = !i.state.showBanDialog;
994 i.state.banType = BanType.Site;
998 handleModBanReasonChange(i: CommentNode, event: any) {
999 i.state.banReason = event.target.value;
1000 i.setState(i.state);
1003 handleModBanExpiresChange(i: CommentNode, event: any) {
1004 i.state.banExpires = event.target.value;
1005 i.setState(i.state);
1008 handleModBanFromCommunitySubmit(i: CommentNode) {
1009 i.state.banType = BanType.Community;
1010 i.setState(i.state);
1011 i.handleModBanBothSubmit(i);
1014 handleModBanSubmit(i: CommentNode) {
1015 i.state.banType = BanType.Site;
1016 i.setState(i.state);
1017 i.handleModBanBothSubmit(i);
1020 handleModBanBothSubmit(i: CommentNode) {
1021 event.preventDefault();
1023 if (i.state.banType == BanType.Community) {
1024 let form: BanFromCommunityForm = {
1025 user_id: i.props.node.comment.creator_id,
1026 community_id: i.props.node.comment.community_id,
1027 ban: !i.props.node.comment.banned_from_community,
1028 reason: i.state.banReason,
1029 expires: getUnixTime(i.state.banExpires),
1031 WebSocketService.Instance.banFromCommunity(form);
1033 let form: BanUserForm = {
1034 user_id: i.props.node.comment.creator_id,
1035 ban: !i.props.node.comment.banned,
1036 reason: i.state.banReason,
1037 expires: getUnixTime(i.state.banExpires),
1039 WebSocketService.Instance.banUser(form);
1042 i.state.showBanDialog = false;
1043 i.setState(i.state);
1046 handleShowConfirmAppointAsMod(i: CommentNode) {
1047 i.state.showConfirmAppointAsMod = true;
1048 i.setState(i.state);
1051 handleCancelConfirmAppointAsMod(i: CommentNode) {
1052 i.state.showConfirmAppointAsMod = false;
1053 i.setState(i.state);
1056 handleAddModToCommunity(i: CommentNode) {
1057 let form: AddModToCommunityForm = {
1058 user_id: i.props.node.comment.creator_id,
1059 community_id: i.props.node.comment.community_id,
1062 WebSocketService.Instance.addModToCommunity(form);
1063 i.state.showConfirmAppointAsMod = false;
1064 i.setState(i.state);
1067 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1068 i.state.showConfirmAppointAsAdmin = true;
1069 i.setState(i.state);
1072 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1073 i.state.showConfirmAppointAsAdmin = false;
1074 i.setState(i.state);
1077 handleAddAdmin(i: CommentNode) {
1078 let form: AddAdminForm = {
1079 user_id: i.props.node.comment.creator_id,
1082 WebSocketService.Instance.addAdmin(form);
1083 i.state.showConfirmAppointAsAdmin = false;
1084 i.setState(i.state);
1087 handleShowConfirmTransferCommunity(i: CommentNode) {
1088 i.state.showConfirmTransferCommunity = true;
1089 i.setState(i.state);
1092 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1093 i.state.showConfirmTransferCommunity = false;
1094 i.setState(i.state);
1097 handleTransferCommunity(i: CommentNode) {
1098 let form: TransferCommunityForm = {
1099 community_id: i.props.node.comment.community_id,
1100 user_id: i.props.node.comment.creator_id,
1102 WebSocketService.Instance.transferCommunity(form);
1103 i.state.showConfirmTransferCommunity = false;
1104 i.setState(i.state);
1107 handleShowConfirmTransferSite(i: CommentNode) {
1108 i.state.showConfirmTransferSite = true;
1109 i.setState(i.state);
1112 handleCancelShowConfirmTransferSite(i: CommentNode) {
1113 i.state.showConfirmTransferSite = false;
1114 i.setState(i.state);
1117 handleTransferSite(i: CommentNode) {
1118 let form: TransferSiteForm = {
1119 user_id: i.props.node.comment.creator_id,
1121 WebSocketService.Instance.transferSite(form);
1122 i.state.showConfirmTransferSite = false;
1123 i.setState(i.state);
1126 get isCommentNew(): boolean {
1127 let now = moment.utc().subtract(10, 'minutes');
1128 let then = moment.utc(this.props.node.comment.published);
1129 return now.isBefore(then);
1132 handleCommentCollapse(i: CommentNode) {
1133 i.state.collapsed = !i.state.collapsed;
1134 i.setState(i.state);
1137 handleViewSource(i: CommentNode) {
1138 i.state.viewSource = !i.state.viewSource;
1139 i.setState(i.state);
1142 handleShowAdvanced(i: CommentNode) {
1143 i.state.showAdvanced = !i.state.showAdvanced;
1144 i.setState(i.state);