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: randomHsl(),
100 constructor(props: any, context: any) {
101 super(props, context);
103 this.state = this.emptyState;
104 this.handleReplyCancel = this.handleReplyCancel.bind(this);
105 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
106 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
109 componentWillReceiveProps(nextProps: CommentNodeProps) {
110 this.state.my_vote = nextProps.node.comment.my_vote;
111 this.state.upvotes = nextProps.node.comment.upvotes;
112 this.state.downvotes = nextProps.node.comment.downvotes;
113 this.state.score = nextProps.node.comment.score;
114 this.setState(this.state);
118 let node = this.props.node;
121 className={`comment ${
122 node.comment.parent_id && !this.props.noIndent ? 'ml-2' : ''
126 id={`comment-${node.comment.id}`}
127 className={`details comment-node mb-1 ${
128 this.isCommentNew ? 'mark' : ''
131 !this.props.noIndent &&
132 `border-left: 1px solid; border-color: ${this.state.borderColor} !important`
135 <div class={`${!this.props.noIndent && 'ml-2'}`}>
136 <ul class="list-inline mb-1 text-muted small">
137 <li className="list-inline-item">
139 className="text-info"
140 to={`/u/${node.comment.creator_name}`}
142 {node.comment.creator_avatar && showAvatars() && (
146 src={pictshareAvatarThumbnail(
147 node.comment.creator_avatar
149 class="rounded-circle mr-1"
152 <span>{node.comment.creator_name}</span>
156 <li className="list-inline-item badge badge-light">
161 <li className="list-inline-item badge badge-light">
165 {this.isPostCreator && (
166 <li className="list-inline-item badge badge-light">
170 {(node.comment.banned_from_community || node.comment.banned) && (
171 <li className="list-inline-item badge badge-danger">
175 <li className="list-inline-item">•</li>
177 class="unselectable pointer mr-2"
178 data-tippy-content={i18n.t('number_of_points', {
179 count: this.state.score,
182 <li className="list-inline-item">
183 <span className={this.scoreColor}>
184 <svg className="small icon icon-inline mr-1">
185 <use xlinkHref="#icon-zap"></use>
190 <li className="list-inline-item">
191 <svg class="small icon icon-inline mr-1">
192 <use xlinkHref="#icon-arrow-up"></use>
196 <li className="list-inline-item">
197 <svg class="small icon icon-inline mr-1">
198 <use xlinkHref="#icon-arrow-down"></use>
200 {this.state.downvotes}
203 {this.props.showCommunity && (
204 <li className="list-inline-item">
205 <span> {i18n.t('to')} </span>
206 <Link to={`/c/${node.comment.community_name}`}>
207 {node.comment.community_name}
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 ? (
224 <use xlinkHref="#icon-plus-square"></use>
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')}
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
300 this.handleCommentDownvote
302 data-tippy-content={i18n.t('downvote')}
305 <use xlinkHref="#icon-arrow-down"></use>
310 <li className="list-inline-item-action">
313 onClick={linkEvent(this, this.handleReplyClick)}
314 data-tippy-content={i18n.t('reply')}
316 <svg class="icon icon-inline">
317 <use xlinkHref="#icon-reply1"></use>
321 {!this.myComment && (
322 <li className="list-inline-item-action">
325 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
326 title={i18n.t('message').toLowerCase()}
329 <use xlinkHref="#icon-mail"></use>
334 <li className="list-inline-item-action">
336 className="text-muted"
337 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
338 title={i18n.t('link')}
340 <svg class="icon icon-inline">
341 <use xlinkHref="#icon-link"></use>
345 {!this.state.showAdvanced ? (
346 <li className="list-inline-item-action">
348 className="unselectable pointer"
349 onClick={linkEvent(this, this.handleShowAdvanced)}
350 data-tippy-content={i18n.t('more')}
352 <svg class="icon icon-inline">
353 <use xlinkHref="#icon-more-vertical"></use>
359 <li className="list-inline-item-action">
364 this.handleSaveCommentClick
373 class={`icon icon-inline ${node.comment.saved &&
376 <use xlinkHref="#icon-star"></use>
380 <li className="list-inline-item-action">
383 onClick={linkEvent(this, this.handleViewSource)}
384 data-tippy-content={i18n.t('view_source')}
387 class={`icon icon-inline ${this.state
388 .viewSource && 'text-success'}`}
390 <use xlinkHref="#icon-file-text"></use>
396 <li className="list-inline-item-action">•</li>
397 <li className="list-inline-item-action">
404 data-tippy-content={i18n.t('edit')}
406 <svg class="icon icon-inline">
407 <use xlinkHref="#icon-edit"></use>
411 <li className="list-inline-item-action">
416 this.handleDeleteClick
419 !node.comment.deleted
425 class={`icon icon-inline ${node.comment
426 .deleted && 'text-danger'}`}
428 <use xlinkHref="#icon-trash"></use>
434 {/* Admins and mods can remove comments */}
435 {(this.canMod || this.canAdmin) && (
437 <li className="list-inline-item-action">
438 {!node.comment.removed ? (
443 this.handleModRemoveShow
453 this.handleModRemoveSubmit
462 {/* Mods can ban from community, and appoint as mods to community */}
466 <li className="list-inline-item-action">
467 {!node.comment.banned_from_community ? (
472 this.handleModBanFromCommunityShow
482 this.handleModBanFromCommunitySubmit
490 {!node.comment.banned_from_community && (
491 <li className="list-inline-item-action">
492 {!this.state.showConfirmAppointAsMod ? (
497 this.handleShowConfirmAppointAsMod
501 ? i18n.t('remove_as_mod')
502 : i18n.t('appoint_as_mod')}
506 <span class="d-inline-block mr-1">
507 {i18n.t('are_you_sure')}
510 class="pointer d-inline-block mr-1"
513 this.handleAddModToCommunity
519 class="pointer d-inline-block"
522 this.handleCancelConfirmAppointAsMod
533 {/* Community creators and admins can transfer community to another mod */}
534 {(this.amCommunityCreator || this.canAdmin) &&
536 <li className="list-inline-item-action">
537 {!this.state.showConfirmTransferCommunity ? (
542 this.handleShowConfirmTransferCommunity
545 {i18n.t('transfer_community')}
549 <span class="d-inline-block mr-1">
550 {i18n.t('are_you_sure')}
553 class="pointer d-inline-block mr-1"
556 this.handleTransferCommunity
562 class="pointer d-inline-block"
566 .handleCancelShowConfirmTransferCommunity
575 {/* Admins can ban from all, and appoint other admins */}
579 <li className="list-inline-item-action">
580 {!node.comment.banned ? (
585 this.handleModBanShow
588 {i18n.t('ban_from_site')}
595 this.handleModBanSubmit
598 {i18n.t('unban_from_site')}
603 {!node.comment.banned && (
604 <li className="list-inline-item-action">
605 {!this.state.showConfirmAppointAsAdmin ? (
610 this.handleShowConfirmAppointAsAdmin
614 ? i18n.t('remove_as_admin')
615 : i18n.t('appoint_as_admin')}
619 <span class="d-inline-block mr-1">
620 {i18n.t('are_you_sure')}
623 class="pointer d-inline-block mr-1"
632 class="pointer d-inline-block"
635 this.handleCancelConfirmAppointAsAdmin
646 {/* Site Creator can transfer to another admin */}
647 {this.amSiteCreator && this.isAdmin && (
648 <li className="list-inline-item-action">
649 {!this.state.showConfirmTransferSite ? (
654 this.handleShowConfirmTransferSite
657 {i18n.t('transfer_site')}
661 <span class="d-inline-block mr-1">
662 {i18n.t('are_you_sure')}
665 class="pointer d-inline-block mr-1"
668 this.handleTransferSite
674 class="pointer d-inline-block"
677 this.handleCancelShowConfirmTransferSite
695 {this.state.showRemoveDialog && (
698 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
702 class="form-control mr-2"
703 placeholder={i18n.t('reason')}
704 value={this.state.removeReason}
705 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
707 <button type="submit" class="btn btn-secondary">
708 {i18n.t('remove_comment')}
712 {this.state.showBanDialog && (
713 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
714 <div class="form-group row">
715 <label class="col-form-label">{i18n.t('reason')}</label>
718 class="form-control mr-2"
719 placeholder={i18n.t('reason')}
720 value={this.state.banReason}
721 onInput={linkEvent(this, this.handleModBanReasonChange)}
724 {/* TODO hold off on expires until later */}
725 {/* <div class="form-group row"> */}
726 {/* <label class="col-form-label">Expires</label> */}
727 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
729 <div class="form-group row">
730 <button type="submit" class="btn btn-secondary">
731 {i18n.t('ban')} {node.comment.creator_name}
736 {this.state.showReply && (
739 onReplyCancel={this.handleReplyCancel}
740 disabled={this.props.locked}
743 {node.children && !this.state.collapsed && (
745 nodes={node.children}
746 locked={this.props.locked}
747 moderators={this.props.moderators}
748 admins={this.props.admins}
749 postCreatorId={this.props.postCreatorId}
750 sort={this.props.sort}
751 sortType={this.props.sortType}
754 {/* A collapsed clearfix */}
755 {this.state.collapsed && <div class="row col-12"></div>}
760 get myComment(): boolean {
762 UserService.Instance.user &&
763 this.props.node.comment.creator_id == UserService.Instance.user.id
767 get isMod(): boolean {
769 this.props.moderators &&
771 this.props.moderators.map(m => m.user_id),
772 this.props.node.comment.creator_id
777 get isAdmin(): boolean {
781 this.props.admins.map(a => a.id),
782 this.props.node.comment.creator_id
787 get isPostCreator(): boolean {
788 return this.props.node.comment.creator_id == this.props.postCreatorId;
791 get canMod(): boolean {
792 if (this.props.admins && this.props.moderators) {
793 let adminsThenMods = this.props.admins
795 .concat(this.props.moderators.map(m => m.user_id));
798 UserService.Instance.user,
800 this.props.node.comment.creator_id
807 get canAdmin(): boolean {
811 UserService.Instance.user,
812 this.props.admins.map(a => a.id),
813 this.props.node.comment.creator_id
818 get amCommunityCreator(): boolean {
820 this.props.moderators &&
821 UserService.Instance.user &&
822 this.props.node.comment.creator_id != UserService.Instance.user.id &&
823 UserService.Instance.user.id == this.props.moderators[0].user_id
827 get amSiteCreator(): boolean {
830 UserService.Instance.user &&
831 this.props.node.comment.creator_id != UserService.Instance.user.id &&
832 UserService.Instance.user.id == this.props.admins[0].id
836 get commentUnlessRemoved(): string {
837 let node = this.props.node;
838 return node.comment.removed
839 ? `*${i18n.t('removed')}*`
840 : node.comment.deleted
841 ? `*${i18n.t('deleted')}*`
842 : node.comment.content;
845 handleReplyClick(i: CommentNode) {
846 i.state.showReply = true;
850 handleEditClick(i: CommentNode) {
851 i.state.showEdit = true;
855 handleDeleteClick(i: CommentNode) {
856 let deleteForm: CommentFormI = {
857 content: i.props.node.comment.content,
858 edit_id: i.props.node.comment.id,
859 creator_id: i.props.node.comment.creator_id,
860 post_id: i.props.node.comment.post_id,
861 parent_id: i.props.node.comment.parent_id,
862 deleted: !i.props.node.comment.deleted,
865 WebSocketService.Instance.editComment(deleteForm);
868 handleSaveCommentClick(i: CommentNode) {
870 i.props.node.comment.saved == undefined
872 : !i.props.node.comment.saved;
873 let form: SaveCommentForm = {
874 comment_id: i.props.node.comment.id,
878 WebSocketService.Instance.saveComment(form);
881 handleReplyCancel() {
882 this.state.showReply = false;
883 this.state.showEdit = false;
884 this.setState(this.state);
887 handleCommentUpvote(i: CommentNodeI) {
888 let new_vote = this.state.my_vote == 1 ? 0 : 1;
890 if (this.state.my_vote == 1) {
892 this.state.upvotes--;
893 } else if (this.state.my_vote == -1) {
894 this.state.downvotes--;
895 this.state.upvotes++;
896 this.state.score += 2;
898 this.state.upvotes++;
902 this.state.my_vote = new_vote;
904 let form: CommentLikeForm = {
905 comment_id: i.comment.id,
906 post_id: i.comment.post_id,
907 score: this.state.my_vote,
910 WebSocketService.Instance.likeComment(form);
911 this.setState(this.state);
914 handleCommentDownvote(i: CommentNodeI) {
915 let new_vote = this.state.my_vote == -1 ? 0 : -1;
917 if (this.state.my_vote == 1) {
918 this.state.score -= 2;
919 this.state.upvotes--;
920 this.state.downvotes++;
921 } else if (this.state.my_vote == -1) {
922 this.state.downvotes--;
925 this.state.downvotes++;
929 this.state.my_vote = new_vote;
931 let form: CommentLikeForm = {
932 comment_id: i.comment.id,
933 post_id: i.comment.post_id,
934 score: this.state.my_vote,
937 WebSocketService.Instance.likeComment(form);
938 this.setState(this.state);
941 handleModRemoveShow(i: CommentNode) {
942 i.state.showRemoveDialog = true;
946 handleModRemoveReasonChange(i: CommentNode, event: any) {
947 i.state.removeReason = event.target.value;
951 handleModRemoveSubmit(i: CommentNode) {
952 event.preventDefault();
953 let form: CommentFormI = {
954 content: i.props.node.comment.content,
955 edit_id: i.props.node.comment.id,
956 creator_id: i.props.node.comment.creator_id,
957 post_id: i.props.node.comment.post_id,
958 parent_id: i.props.node.comment.parent_id,
959 removed: !i.props.node.comment.removed,
960 reason: i.state.removeReason,
963 WebSocketService.Instance.editComment(form);
965 i.state.showRemoveDialog = false;
969 handleMarkRead(i: CommentNode) {
970 // if it has a user_mention_id field, then its a mention
971 if (i.props.node.comment.user_mention_id) {
972 let form: EditUserMentionForm = {
973 user_mention_id: i.props.node.comment.user_mention_id,
974 read: !i.props.node.comment.read,
976 WebSocketService.Instance.editUserMention(form);
978 let form: CommentFormI = {
979 content: i.props.node.comment.content,
980 edit_id: i.props.node.comment.id,
981 creator_id: i.props.node.comment.creator_id,
982 post_id: i.props.node.comment.post_id,
983 parent_id: i.props.node.comment.parent_id,
984 read: !i.props.node.comment.read,
987 WebSocketService.Instance.editComment(form);
991 handleModBanFromCommunityShow(i: CommentNode) {
992 i.state.showBanDialog = !i.state.showBanDialog;
993 i.state.banType = BanType.Community;
997 handleModBanShow(i: CommentNode) {
998 i.state.showBanDialog = !i.state.showBanDialog;
999 i.state.banType = BanType.Site;
1000 i.setState(i.state);
1003 handleModBanReasonChange(i: CommentNode, event: any) {
1004 i.state.banReason = event.target.value;
1005 i.setState(i.state);
1008 handleModBanExpiresChange(i: CommentNode, event: any) {
1009 i.state.banExpires = event.target.value;
1010 i.setState(i.state);
1013 handleModBanFromCommunitySubmit(i: CommentNode) {
1014 i.state.banType = BanType.Community;
1015 i.setState(i.state);
1016 i.handleModBanBothSubmit(i);
1019 handleModBanSubmit(i: CommentNode) {
1020 i.state.banType = BanType.Site;
1021 i.setState(i.state);
1022 i.handleModBanBothSubmit(i);
1025 handleModBanBothSubmit(i: CommentNode) {
1026 event.preventDefault();
1028 if (i.state.banType == BanType.Community) {
1029 let form: BanFromCommunityForm = {
1030 user_id: i.props.node.comment.creator_id,
1031 community_id: i.props.node.comment.community_id,
1032 ban: !i.props.node.comment.banned_from_community,
1033 reason: i.state.banReason,
1034 expires: getUnixTime(i.state.banExpires),
1036 WebSocketService.Instance.banFromCommunity(form);
1038 let form: BanUserForm = {
1039 user_id: i.props.node.comment.creator_id,
1040 ban: !i.props.node.comment.banned,
1041 reason: i.state.banReason,
1042 expires: getUnixTime(i.state.banExpires),
1044 WebSocketService.Instance.banUser(form);
1047 i.state.showBanDialog = false;
1048 i.setState(i.state);
1051 handleShowConfirmAppointAsMod(i: CommentNode) {
1052 i.state.showConfirmAppointAsMod = true;
1053 i.setState(i.state);
1056 handleCancelConfirmAppointAsMod(i: CommentNode) {
1057 i.state.showConfirmAppointAsMod = false;
1058 i.setState(i.state);
1061 handleAddModToCommunity(i: CommentNode) {
1062 let form: AddModToCommunityForm = {
1063 user_id: i.props.node.comment.creator_id,
1064 community_id: i.props.node.comment.community_id,
1067 WebSocketService.Instance.addModToCommunity(form);
1068 i.state.showConfirmAppointAsMod = false;
1069 i.setState(i.state);
1072 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1073 i.state.showConfirmAppointAsAdmin = true;
1074 i.setState(i.state);
1077 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1078 i.state.showConfirmAppointAsAdmin = false;
1079 i.setState(i.state);
1082 handleAddAdmin(i: CommentNode) {
1083 let form: AddAdminForm = {
1084 user_id: i.props.node.comment.creator_id,
1087 WebSocketService.Instance.addAdmin(form);
1088 i.state.showConfirmAppointAsAdmin = false;
1089 i.setState(i.state);
1092 handleShowConfirmTransferCommunity(i: CommentNode) {
1093 i.state.showConfirmTransferCommunity = true;
1094 i.setState(i.state);
1097 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1098 i.state.showConfirmTransferCommunity = false;
1099 i.setState(i.state);
1102 handleTransferCommunity(i: CommentNode) {
1103 let form: TransferCommunityForm = {
1104 community_id: i.props.node.comment.community_id,
1105 user_id: i.props.node.comment.creator_id,
1107 WebSocketService.Instance.transferCommunity(form);
1108 i.state.showConfirmTransferCommunity = false;
1109 i.setState(i.state);
1112 handleShowConfirmTransferSite(i: CommentNode) {
1113 i.state.showConfirmTransferSite = true;
1114 i.setState(i.state);
1117 handleCancelShowConfirmTransferSite(i: CommentNode) {
1118 i.state.showConfirmTransferSite = false;
1119 i.setState(i.state);
1122 handleTransferSite(i: CommentNode) {
1123 let form: TransferSiteForm = {
1124 user_id: i.props.node.comment.creator_id,
1126 WebSocketService.Instance.transferSite(form);
1127 i.state.showConfirmTransferSite = false;
1128 i.setState(i.state);
1131 get isCommentNew(): boolean {
1132 let now = moment.utc().subtract(10, 'minutes');
1133 let then = moment.utc(this.props.node.comment.published);
1134 return now.isBefore(then);
1137 handleCommentCollapse(i: CommentNode) {
1138 i.state.collapsed = !i.state.collapsed;
1139 i.setState(i.state);
1142 handleViewSource(i: CommentNode) {
1143 i.state.viewSource = !i.state.viewSource;
1144 i.setState(i.state);
1147 handleShowAdvanced(i: CommentNode) {
1148 i.state.showAdvanced = !i.state.showAdvanced;
1149 i.setState(i.state);
1154 if (this.state.my_vote == 1) {
1156 } else if (this.state.my_vote == -1) {
1157 return 'text-danger';
1159 return 'text-muted';