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">
177 <li className="list-inline-item">•</li>
179 class="unselectable pointer mr-2"
180 data-tippy-content={i18n.t('number_of_points', {
181 count: this.state.score,
184 <li className="list-inline-item">
185 <span className={this.scoreColor}>
186 <svg className="small icon icon-inline mr-1">
187 <use xlinkHref="#icon-zap"></use>
192 <li className="list-inline-item">
193 <svg class="small icon icon-inline mr-1">
194 <use xlinkHref="#icon-arrow-up"></use>
198 <li className="list-inline-item">
199 <svg class="small icon icon-inline mr-1">
200 <use xlinkHref="#icon-arrow-down"></use>
202 {this.state.downvotes}
205 {this.props.showCommunity && (
206 <li className="list-inline-item">
207 <span> {i18n.t('to')} </span>
208 <Link to={`/c/${node.comment.community_name}`}>
209 {node.comment.community_name}
213 <li className="list-inline-item">•</li>
214 <li className="list-inline-item">
216 <MomentTime data={node.comment} />
219 <li className="list-inline-item">
221 className="unselectable pointer text-monospace"
222 onClick={linkEvent(this, this.handleCommentCollapse)}
224 {this.state.collapsed ? (
226 <use xlinkHref="#icon-plus-square"></use>
230 <use xlinkHref="#icon-minus-square"></use>
236 {this.state.showEdit && (
240 onReplyCancel={this.handleReplyCancel}
241 disabled={this.props.locked}
244 {!this.state.showEdit && !this.state.collapsed && (
246 {this.state.viewSource ? (
247 <pre>{this.commentUnlessRemoved}</pre>
251 dangerouslySetInnerHTML={mdToHtml(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')}
286 <use xlinkHref="#icon-arrow-up"></use>
290 {WebSocketService.Instance.site.enable_downvotes && (
291 <li className="list-inline-item-action">
293 className={`vote-animate btn btn-link p-0 mb-1 ${
294 this.state.my_vote == -1
298 onClick={linkEvent(node, this.handleCommentDownvote)}
299 data-tippy-content={i18n.t('downvote')}
302 <use xlinkHref="#icon-arrow-down"></use>
307 <li className="list-inline-item-action">
310 onClick={linkEvent(this, this.handleReplyClick)}
311 data-tippy-content={i18n.t('reply')}
313 <svg class="icon icon-inline">
314 <use xlinkHref="#icon-reply1"></use>
318 {!this.myComment && (
319 <li className="list-inline-item-action">
322 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
323 title={i18n.t('message').toLowerCase()}
326 <use xlinkHref="#icon-mail"></use>
331 <li className="list-inline-item-action">
333 className="text-muted"
334 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
335 title={i18n.t('link')}
337 <svg class="icon icon-inline">
338 <use xlinkHref="#icon-link"></use>
342 {!this.state.showAdvanced ? (
343 <li className="list-inline-item-action">
345 className="unselectable pointer"
346 onClick={linkEvent(this, this.handleShowAdvanced)}
347 data-tippy-content={i18n.t('more')}
349 <svg class="icon icon-inline">
350 <use xlinkHref="#icon-more-vertical"></use>
356 <li className="list-inline-item-action">
361 this.handleSaveCommentClick
370 class={`icon icon-inline ${node.comment.saved &&
373 <use xlinkHref="#icon-star"></use>
377 <li className="list-inline-item-action">
380 onClick={linkEvent(this, this.handleViewSource)}
381 data-tippy-content={i18n.t('view_source')}
384 class={`icon icon-inline ${this.state
385 .viewSource && 'text-success'}`}
387 <use xlinkHref="#icon-file-text"></use>
393 <li className="list-inline-item-action">•</li>
394 <li className="list-inline-item-action">
397 onClick={linkEvent(this, this.handleEditClick)}
398 data-tippy-content={i18n.t('edit')}
400 <svg class="icon icon-inline">
401 <use xlinkHref="#icon-edit"></use>
405 <li className="list-inline-item-action">
410 this.handleDeleteClick
413 !node.comment.deleted
419 class={`icon icon-inline ${node.comment
420 .deleted && 'text-danger'}`}
422 <use xlinkHref="#icon-trash"></use>
428 {/* Admins and mods can remove comments */}
429 {(this.canMod || this.canAdmin) && (
431 <li className="list-inline-item-action">
432 {!node.comment.removed ? (
437 this.handleModRemoveShow
447 this.handleModRemoveSubmit
456 {/* Mods can ban from community, and appoint as mods to community */}
460 <li className="list-inline-item-action">
461 {!node.comment.banned_from_community ? (
466 this.handleModBanFromCommunityShow
476 this.handleModBanFromCommunitySubmit
484 {!node.comment.banned_from_community && (
485 <li className="list-inline-item-action">
486 {!this.state.showConfirmAppointAsMod ? (
491 this.handleShowConfirmAppointAsMod
495 ? i18n.t('remove_as_mod')
496 : i18n.t('appoint_as_mod')}
500 <span class="d-inline-block mr-1">
501 {i18n.t('are_you_sure')}
504 class="pointer d-inline-block mr-1"
507 this.handleAddModToCommunity
513 class="pointer d-inline-block"
516 this.handleCancelConfirmAppointAsMod
527 {/* Community creators and admins can transfer community to another mod */}
528 {(this.amCommunityCreator || this.canAdmin) &&
530 <li className="list-inline-item-action">
531 {!this.state.showConfirmTransferCommunity ? (
536 this.handleShowConfirmTransferCommunity
539 {i18n.t('transfer_community')}
543 <span class="d-inline-block mr-1">
544 {i18n.t('are_you_sure')}
547 class="pointer d-inline-block mr-1"
550 this.handleTransferCommunity
556 class="pointer d-inline-block"
560 .handleCancelShowConfirmTransferCommunity
569 {/* Admins can ban from all, and appoint other admins */}
573 <li className="list-inline-item-action">
574 {!node.comment.banned ? (
579 this.handleModBanShow
582 {i18n.t('ban_from_site')}
589 this.handleModBanSubmit
592 {i18n.t('unban_from_site')}
597 {!node.comment.banned && (
598 <li className="list-inline-item-action">
599 {!this.state.showConfirmAppointAsAdmin ? (
604 this.handleShowConfirmAppointAsAdmin
608 ? i18n.t('remove_as_admin')
609 : i18n.t('appoint_as_admin')}
613 <span class="d-inline-block mr-1">
614 {i18n.t('are_you_sure')}
617 class="pointer d-inline-block mr-1"
626 class="pointer d-inline-block"
629 this.handleCancelConfirmAppointAsAdmin
640 {/* Site Creator can transfer to another admin */}
641 {this.amSiteCreator && this.isAdmin && (
642 <li className="list-inline-item-action">
643 {!this.state.showConfirmTransferSite ? (
648 this.handleShowConfirmTransferSite
651 {i18n.t('transfer_site')}
655 <span class="d-inline-block mr-1">
656 {i18n.t('are_you_sure')}
659 class="pointer d-inline-block mr-1"
662 this.handleTransferSite
668 class="pointer d-inline-block"
671 this.handleCancelShowConfirmTransferSite
688 {this.state.showRemoveDialog && (
691 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
695 class="form-control mr-2"
696 placeholder={i18n.t('reason')}
697 value={this.state.removeReason}
698 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
700 <button type="submit" class="btn btn-secondary">
701 {i18n.t('remove_comment')}
705 {this.state.showBanDialog && (
706 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
707 <div class="form-group row">
708 <label class="col-form-label">{i18n.t('reason')}</label>
711 class="form-control mr-2"
712 placeholder={i18n.t('reason')}
713 value={this.state.banReason}
714 onInput={linkEvent(this, this.handleModBanReasonChange)}
717 {/* TODO hold off on expires until later */}
718 {/* <div class="form-group row"> */}
719 {/* <label class="col-form-label">Expires</label> */}
720 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
722 <div class="form-group row">
723 <button type="submit" class="btn btn-secondary">
724 {i18n.t('ban')} {node.comment.creator_name}
729 {this.state.showReply && (
732 onReplyCancel={this.handleReplyCancel}
733 disabled={this.props.locked}
736 {node.children && !this.state.collapsed && (
738 nodes={node.children}
739 locked={this.props.locked}
740 moderators={this.props.moderators}
741 admins={this.props.admins}
742 postCreatorId={this.props.postCreatorId}
743 sort={this.props.sort}
744 sortType={this.props.sortType}
747 {/* A collapsed clearfix */}
748 {this.state.collapsed && <div class="row col-12"></div>}
753 get myComment(): boolean {
755 UserService.Instance.user &&
756 this.props.node.comment.creator_id == UserService.Instance.user.id
760 get isMod(): boolean {
762 this.props.moderators &&
764 this.props.moderators.map(m => m.user_id),
765 this.props.node.comment.creator_id
770 get isAdmin(): boolean {
774 this.props.admins.map(a => a.id),
775 this.props.node.comment.creator_id
780 get isPostCreator(): boolean {
781 return this.props.node.comment.creator_id == this.props.postCreatorId;
784 get canMod(): boolean {
785 if (this.props.admins && this.props.moderators) {
786 let adminsThenMods = this.props.admins
788 .concat(this.props.moderators.map(m => m.user_id));
791 UserService.Instance.user,
793 this.props.node.comment.creator_id
800 get canAdmin(): boolean {
804 UserService.Instance.user,
805 this.props.admins.map(a => a.id),
806 this.props.node.comment.creator_id
811 get amCommunityCreator(): boolean {
813 this.props.moderators &&
814 UserService.Instance.user &&
815 this.props.node.comment.creator_id != UserService.Instance.user.id &&
816 UserService.Instance.user.id == this.props.moderators[0].user_id
820 get amSiteCreator(): boolean {
823 UserService.Instance.user &&
824 this.props.node.comment.creator_id != UserService.Instance.user.id &&
825 UserService.Instance.user.id == this.props.admins[0].id
829 get commentUnlessRemoved(): string {
830 let node = this.props.node;
831 return node.comment.removed
832 ? `*${i18n.t('removed')}*`
833 : node.comment.deleted
834 ? `*${i18n.t('deleted')}*`
835 : node.comment.content;
838 handleReplyClick(i: CommentNode) {
839 i.state.showReply = true;
843 handleEditClick(i: CommentNode) {
844 i.state.showEdit = true;
848 handleDeleteClick(i: CommentNode) {
849 let deleteForm: CommentFormI = {
850 content: i.props.node.comment.content,
851 edit_id: i.props.node.comment.id,
852 creator_id: i.props.node.comment.creator_id,
853 post_id: i.props.node.comment.post_id,
854 parent_id: i.props.node.comment.parent_id,
855 deleted: !i.props.node.comment.deleted,
858 WebSocketService.Instance.editComment(deleteForm);
861 handleSaveCommentClick(i: CommentNode) {
863 i.props.node.comment.saved == undefined
865 : !i.props.node.comment.saved;
866 let form: SaveCommentForm = {
867 comment_id: i.props.node.comment.id,
871 WebSocketService.Instance.saveComment(form);
874 handleReplyCancel() {
875 this.state.showReply = false;
876 this.state.showEdit = false;
877 this.setState(this.state);
880 handleCommentUpvote(i: CommentNodeI) {
881 let new_vote = this.state.my_vote == 1 ? 0 : 1;
883 if (this.state.my_vote == 1) {
885 this.state.upvotes--;
886 } else if (this.state.my_vote == -1) {
887 this.state.downvotes--;
888 this.state.upvotes++;
889 this.state.score += 2;
891 this.state.upvotes++;
895 this.state.my_vote = new_vote;
897 let form: CommentLikeForm = {
898 comment_id: i.comment.id,
899 post_id: i.comment.post_id,
900 score: this.state.my_vote,
903 WebSocketService.Instance.likeComment(form);
904 this.setState(this.state);
907 handleCommentDownvote(i: CommentNodeI) {
908 let new_vote = this.state.my_vote == -1 ? 0 : -1;
910 if (this.state.my_vote == 1) {
911 this.state.score -= 2;
912 this.state.upvotes--;
913 this.state.downvotes++;
914 } else if (this.state.my_vote == -1) {
915 this.state.downvotes--;
918 this.state.downvotes++;
922 this.state.my_vote = new_vote;
924 let form: CommentLikeForm = {
925 comment_id: i.comment.id,
926 post_id: i.comment.post_id,
927 score: this.state.my_vote,
930 WebSocketService.Instance.likeComment(form);
931 this.setState(this.state);
934 handleModRemoveShow(i: CommentNode) {
935 i.state.showRemoveDialog = true;
939 handleModRemoveReasonChange(i: CommentNode, event: any) {
940 i.state.removeReason = event.target.value;
944 handleModRemoveSubmit(i: CommentNode) {
945 event.preventDefault();
946 let form: CommentFormI = {
947 content: i.props.node.comment.content,
948 edit_id: i.props.node.comment.id,
949 creator_id: i.props.node.comment.creator_id,
950 post_id: i.props.node.comment.post_id,
951 parent_id: i.props.node.comment.parent_id,
952 removed: !i.props.node.comment.removed,
953 reason: i.state.removeReason,
956 WebSocketService.Instance.editComment(form);
958 i.state.showRemoveDialog = false;
962 handleMarkRead(i: CommentNode) {
963 // if it has a user_mention_id field, then its a mention
964 if (i.props.node.comment.user_mention_id) {
965 let form: EditUserMentionForm = {
966 user_mention_id: i.props.node.comment.user_mention_id,
967 read: !i.props.node.comment.read,
969 WebSocketService.Instance.editUserMention(form);
971 let form: CommentFormI = {
972 content: i.props.node.comment.content,
973 edit_id: i.props.node.comment.id,
974 creator_id: i.props.node.comment.creator_id,
975 post_id: i.props.node.comment.post_id,
976 parent_id: i.props.node.comment.parent_id,
977 read: !i.props.node.comment.read,
980 WebSocketService.Instance.editComment(form);
984 handleModBanFromCommunityShow(i: CommentNode) {
985 i.state.showBanDialog = !i.state.showBanDialog;
986 i.state.banType = BanType.Community;
990 handleModBanShow(i: CommentNode) {
991 i.state.showBanDialog = !i.state.showBanDialog;
992 i.state.banType = BanType.Site;
996 handleModBanReasonChange(i: CommentNode, event: any) {
997 i.state.banReason = event.target.value;
1001 handleModBanExpiresChange(i: CommentNode, event: any) {
1002 i.state.banExpires = event.target.value;
1003 i.setState(i.state);
1006 handleModBanFromCommunitySubmit(i: CommentNode) {
1007 i.state.banType = BanType.Community;
1008 i.setState(i.state);
1009 i.handleModBanBothSubmit(i);
1012 handleModBanSubmit(i: CommentNode) {
1013 i.state.banType = BanType.Site;
1014 i.setState(i.state);
1015 i.handleModBanBothSubmit(i);
1018 handleModBanBothSubmit(i: CommentNode) {
1019 event.preventDefault();
1021 if (i.state.banType == BanType.Community) {
1022 let form: BanFromCommunityForm = {
1023 user_id: i.props.node.comment.creator_id,
1024 community_id: i.props.node.comment.community_id,
1025 ban: !i.props.node.comment.banned_from_community,
1026 reason: i.state.banReason,
1027 expires: getUnixTime(i.state.banExpires),
1029 WebSocketService.Instance.banFromCommunity(form);
1031 let form: BanUserForm = {
1032 user_id: i.props.node.comment.creator_id,
1033 ban: !i.props.node.comment.banned,
1034 reason: i.state.banReason,
1035 expires: getUnixTime(i.state.banExpires),
1037 WebSocketService.Instance.banUser(form);
1040 i.state.showBanDialog = false;
1041 i.setState(i.state);
1044 handleShowConfirmAppointAsMod(i: CommentNode) {
1045 i.state.showConfirmAppointAsMod = true;
1046 i.setState(i.state);
1049 handleCancelConfirmAppointAsMod(i: CommentNode) {
1050 i.state.showConfirmAppointAsMod = false;
1051 i.setState(i.state);
1054 handleAddModToCommunity(i: CommentNode) {
1055 let form: AddModToCommunityForm = {
1056 user_id: i.props.node.comment.creator_id,
1057 community_id: i.props.node.comment.community_id,
1060 WebSocketService.Instance.addModToCommunity(form);
1061 i.state.showConfirmAppointAsMod = false;
1062 i.setState(i.state);
1065 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1066 i.state.showConfirmAppointAsAdmin = true;
1067 i.setState(i.state);
1070 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1071 i.state.showConfirmAppointAsAdmin = false;
1072 i.setState(i.state);
1075 handleAddAdmin(i: CommentNode) {
1076 let form: AddAdminForm = {
1077 user_id: i.props.node.comment.creator_id,
1080 WebSocketService.Instance.addAdmin(form);
1081 i.state.showConfirmAppointAsAdmin = false;
1082 i.setState(i.state);
1085 handleShowConfirmTransferCommunity(i: CommentNode) {
1086 i.state.showConfirmTransferCommunity = true;
1087 i.setState(i.state);
1090 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1091 i.state.showConfirmTransferCommunity = false;
1092 i.setState(i.state);
1095 handleTransferCommunity(i: CommentNode) {
1096 let form: TransferCommunityForm = {
1097 community_id: i.props.node.comment.community_id,
1098 user_id: i.props.node.comment.creator_id,
1100 WebSocketService.Instance.transferCommunity(form);
1101 i.state.showConfirmTransferCommunity = false;
1102 i.setState(i.state);
1105 handleShowConfirmTransferSite(i: CommentNode) {
1106 i.state.showConfirmTransferSite = true;
1107 i.setState(i.state);
1110 handleCancelShowConfirmTransferSite(i: CommentNode) {
1111 i.state.showConfirmTransferSite = false;
1112 i.setState(i.state);
1115 handleTransferSite(i: CommentNode) {
1116 let form: TransferSiteForm = {
1117 user_id: i.props.node.comment.creator_id,
1119 WebSocketService.Instance.transferSite(form);
1120 i.state.showConfirmTransferSite = false;
1121 i.setState(i.state);
1124 get isCommentNew(): boolean {
1125 let now = moment.utc().subtract(10, 'minutes');
1126 let then = moment.utc(this.props.node.comment.published);
1127 return now.isBefore(then);
1130 handleCommentCollapse(i: CommentNode) {
1131 i.state.collapsed = !i.state.collapsed;
1132 i.setState(i.state);
1135 handleViewSource(i: CommentNode) {
1136 i.state.viewSource = !i.state.viewSource;
1137 i.setState(i.state);
1140 handleShowAdvanced(i: CommentNode) {
1141 i.state.showAdvanced = !i.state.showAdvanced;
1142 i.setState(i.state);
1147 if (this.state.my_vote == 1) {
1149 } else if (this.state.my_vote == -1) {
1150 return 'text-danger';
1152 return 'text-muted';