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={this.scoreColor}>
185 <svg className="small icon icon-inline mr-1">
186 <use xlinkHref="#icon-zap"></use>
191 <li className="list-inline-item">
192 <svg class="small icon icon-inline mr-1">
193 <use xlinkHref="#icon-arrow-up"></use>
197 <li className="list-inline-item">
198 <svg class="small icon icon-inline mr-1">
199 <use xlinkHref="#icon-arrow-down"></use>
201 {this.state.downvotes}
204 {this.props.showCommunity && (
205 <li className="list-inline-item">
206 <span> {i18n.t('to')} </span>
207 <Link to={`/c/${node.comment.community_name}`}>
208 {node.comment.community_name}
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(this.commentUnlessRemoved)}
252 <ul class="list-inline mb-0 text-muted font-weight-bold h5">
253 {this.props.markable && (
254 <li className="list-inline-item-action">
257 onClick={linkEvent(this, this.handleMarkRead)}
260 ? i18n.t('mark_as_unread')
261 : i18n.t('mark_as_read')
265 class={`icon icon-inline ${node.comment.read &&
268 <use xlinkHref="#icon-check"></use>
273 {UserService.Instance.user && !this.props.viewOnly && (
275 <li className="list-inline-item-action">
277 className={`vote-animate btn btn-link p-0 mb-1 ${
278 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
280 onClick={linkEvent(node, this.handleCommentUpvote)}
281 data-tippy-content={i18n.t('upvote')}
284 <use xlinkHref="#icon-arrow-up"></use>
288 {WebSocketService.Instance.site.enable_downvotes && (
289 <li className="list-inline-item-action">
291 className={`vote-animate btn btn-link p-0 mb-1 ${
292 this.state.my_vote == -1
296 onClick={linkEvent(node, this.handleCommentDownvote)}
297 data-tippy-content={i18n.t('downvote')}
300 <use xlinkHref="#icon-arrow-down"></use>
305 <li className="list-inline-item-action">
308 onClick={linkEvent(this, this.handleReplyClick)}
309 data-tippy-content={i18n.t('reply')}
311 <svg class="icon icon-inline">
312 <use xlinkHref="#icon-reply1"></use>
316 {!this.myComment && (
317 <li className="list-inline-item-action">
320 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
321 title={i18n.t('message').toLowerCase()}
324 <use xlinkHref="#icon-mail"></use>
329 <li className="list-inline-item-action">
331 className="text-muted"
332 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
333 title={i18n.t('link')}
335 <svg class="icon icon-inline">
336 <use xlinkHref="#icon-link"></use>
340 {!this.state.showAdvanced ? (
341 <li className="list-inline-item-action">
343 className="unselectable pointer"
344 onClick={linkEvent(this, this.handleShowAdvanced)}
345 data-tippy-content={i18n.t('more')}
347 <svg class="icon icon-inline">
348 <use xlinkHref="#icon-more-vertical"></use>
354 <li className="list-inline-item-action">
359 this.handleSaveCommentClick
368 class={`icon icon-inline ${node.comment.saved &&
371 <use xlinkHref="#icon-star"></use>
375 <li className="list-inline-item-action">
378 onClick={linkEvent(this, this.handleViewSource)}
379 data-tippy-content={i18n.t('view_source')}
382 class={`icon icon-inline ${this.state
383 .viewSource && 'text-success'}`}
385 <use xlinkHref="#icon-file-text"></use>
391 <li className="list-inline-item-action">•</li>
392 <li className="list-inline-item-action">
395 onClick={linkEvent(this, this.handleEditClick)}
396 data-tippy-content={i18n.t('edit')}
398 <svg class="icon icon-inline">
399 <use xlinkHref="#icon-edit"></use>
403 <li className="list-inline-item-action">
408 this.handleDeleteClick
411 !node.comment.deleted
417 class={`icon icon-inline ${node.comment
418 .deleted && 'text-danger'}`}
420 <use xlinkHref="#icon-trash"></use>
426 {/* Admins and mods can remove comments */}
427 {(this.canMod || this.canAdmin) && (
429 <li className="list-inline-item-action">
430 {!node.comment.removed ? (
435 this.handleModRemoveShow
445 this.handleModRemoveSubmit
454 {/* Mods can ban from community, and appoint as mods to community */}
458 <li className="list-inline-item-action">
459 {!node.comment.banned_from_community ? (
464 this.handleModBanFromCommunityShow
474 this.handleModBanFromCommunitySubmit
482 {!node.comment.banned_from_community && (
483 <li className="list-inline-item-action">
484 {!this.state.showConfirmAppointAsMod ? (
489 this.handleShowConfirmAppointAsMod
493 ? i18n.t('remove_as_mod')
494 : i18n.t('appoint_as_mod')}
498 <span class="d-inline-block mr-1">
499 {i18n.t('are_you_sure')}
502 class="pointer d-inline-block mr-1"
505 this.handleAddModToCommunity
511 class="pointer d-inline-block"
514 this.handleCancelConfirmAppointAsMod
525 {/* Community creators and admins can transfer community to another mod */}
526 {(this.amCommunityCreator || this.canAdmin) &&
528 <li className="list-inline-item-action">
529 {!this.state.showConfirmTransferCommunity ? (
534 this.handleShowConfirmTransferCommunity
537 {i18n.t('transfer_community')}
541 <span class="d-inline-block mr-1">
542 {i18n.t('are_you_sure')}
545 class="pointer d-inline-block mr-1"
548 this.handleTransferCommunity
554 class="pointer d-inline-block"
558 .handleCancelShowConfirmTransferCommunity
567 {/* Admins can ban from all, and appoint other admins */}
571 <li className="list-inline-item-action">
572 {!node.comment.banned ? (
577 this.handleModBanShow
580 {i18n.t('ban_from_site')}
587 this.handleModBanSubmit
590 {i18n.t('unban_from_site')}
595 {!node.comment.banned && (
596 <li className="list-inline-item-action">
597 {!this.state.showConfirmAppointAsAdmin ? (
602 this.handleShowConfirmAppointAsAdmin
606 ? i18n.t('remove_as_admin')
607 : i18n.t('appoint_as_admin')}
611 <span class="d-inline-block mr-1">
612 {i18n.t('are_you_sure')}
615 class="pointer d-inline-block mr-1"
624 class="pointer d-inline-block"
627 this.handleCancelConfirmAppointAsAdmin
638 {/* Site Creator can transfer to another admin */}
639 {this.amSiteCreator && this.isAdmin && (
640 <li className="list-inline-item-action">
641 {!this.state.showConfirmTransferSite ? (
646 this.handleShowConfirmTransferSite
649 {i18n.t('transfer_site')}
653 <span class="d-inline-block mr-1">
654 {i18n.t('are_you_sure')}
657 class="pointer d-inline-block mr-1"
660 this.handleTransferSite
666 class="pointer d-inline-block"
669 this.handleCancelShowConfirmTransferSite
686 {this.state.showRemoveDialog && (
689 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
693 class="form-control mr-2"
694 placeholder={i18n.t('reason')}
695 value={this.state.removeReason}
696 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
698 <button type="submit" class="btn btn-secondary">
699 {i18n.t('remove_comment')}
703 {this.state.showBanDialog && (
704 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
705 <div class="form-group row">
706 <label class="col-form-label">{i18n.t('reason')}</label>
709 class="form-control mr-2"
710 placeholder={i18n.t('reason')}
711 value={this.state.banReason}
712 onInput={linkEvent(this, this.handleModBanReasonChange)}
715 {/* TODO hold off on expires until later */}
716 {/* <div class="form-group row"> */}
717 {/* <label class="col-form-label">Expires</label> */}
718 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
720 <div class="form-group row">
721 <button type="submit" class="btn btn-secondary">
722 {i18n.t('ban')} {node.comment.creator_name}
727 {this.state.showReply && (
730 onReplyCancel={this.handleReplyCancel}
731 disabled={this.props.locked}
734 {node.children && !this.state.collapsed && (
736 nodes={node.children}
737 locked={this.props.locked}
738 moderators={this.props.moderators}
739 admins={this.props.admins}
740 postCreatorId={this.props.postCreatorId}
741 sort={this.props.sort}
742 sortType={this.props.sortType}
745 {/* A collapsed clearfix */}
746 {this.state.collapsed && <div class="row col-12"></div>}
751 get myComment(): boolean {
753 UserService.Instance.user &&
754 this.props.node.comment.creator_id == UserService.Instance.user.id
758 get isMod(): boolean {
760 this.props.moderators &&
762 this.props.moderators.map(m => m.user_id),
763 this.props.node.comment.creator_id
768 get isAdmin(): boolean {
772 this.props.admins.map(a => a.id),
773 this.props.node.comment.creator_id
778 get isPostCreator(): boolean {
779 return this.props.node.comment.creator_id == this.props.postCreatorId;
782 get canMod(): boolean {
783 if (this.props.admins && this.props.moderators) {
784 let adminsThenMods = this.props.admins
786 .concat(this.props.moderators.map(m => m.user_id));
789 UserService.Instance.user,
791 this.props.node.comment.creator_id
798 get canAdmin(): boolean {
802 UserService.Instance.user,
803 this.props.admins.map(a => a.id),
804 this.props.node.comment.creator_id
809 get amCommunityCreator(): boolean {
811 this.props.moderators &&
812 UserService.Instance.user &&
813 this.props.node.comment.creator_id != UserService.Instance.user.id &&
814 UserService.Instance.user.id == this.props.moderators[0].user_id
818 get amSiteCreator(): boolean {
821 UserService.Instance.user &&
822 this.props.node.comment.creator_id != UserService.Instance.user.id &&
823 UserService.Instance.user.id == this.props.admins[0].id
827 get commentUnlessRemoved(): string {
828 let node = this.props.node;
829 return node.comment.removed
830 ? `*${i18n.t('removed')}*`
831 : node.comment.deleted
832 ? `*${i18n.t('deleted')}*`
833 : node.comment.content;
836 handleReplyClick(i: CommentNode) {
837 i.state.showReply = true;
841 handleEditClick(i: CommentNode) {
842 i.state.showEdit = true;
846 handleDeleteClick(i: CommentNode) {
847 let deleteForm: CommentFormI = {
848 content: i.props.node.comment.content,
849 edit_id: i.props.node.comment.id,
850 creator_id: i.props.node.comment.creator_id,
851 post_id: i.props.node.comment.post_id,
852 parent_id: i.props.node.comment.parent_id,
853 deleted: !i.props.node.comment.deleted,
856 WebSocketService.Instance.editComment(deleteForm);
859 handleSaveCommentClick(i: CommentNode) {
861 i.props.node.comment.saved == undefined
863 : !i.props.node.comment.saved;
864 let form: SaveCommentForm = {
865 comment_id: i.props.node.comment.id,
869 WebSocketService.Instance.saveComment(form);
872 handleReplyCancel() {
873 this.state.showReply = false;
874 this.state.showEdit = false;
875 this.setState(this.state);
878 handleCommentUpvote(i: CommentNodeI) {
879 let new_vote = this.state.my_vote == 1 ? 0 : 1;
881 if (this.state.my_vote == 1) {
883 this.state.upvotes--;
884 } else if (this.state.my_vote == -1) {
885 this.state.downvotes--;
886 this.state.upvotes++;
887 this.state.score += 2;
889 this.state.upvotes++;
893 this.state.my_vote = new_vote;
895 let form: CommentLikeForm = {
896 comment_id: i.comment.id,
897 post_id: i.comment.post_id,
898 score: this.state.my_vote,
901 WebSocketService.Instance.likeComment(form);
902 this.setState(this.state);
905 handleCommentDownvote(i: CommentNodeI) {
906 let new_vote = this.state.my_vote == -1 ? 0 : -1;
908 if (this.state.my_vote == 1) {
909 this.state.score -= 2;
910 this.state.upvotes--;
911 this.state.downvotes++;
912 } else if (this.state.my_vote == -1) {
913 this.state.downvotes--;
916 this.state.downvotes++;
920 this.state.my_vote = new_vote;
922 let form: CommentLikeForm = {
923 comment_id: i.comment.id,
924 post_id: i.comment.post_id,
925 score: this.state.my_vote,
928 WebSocketService.Instance.likeComment(form);
929 this.setState(this.state);
932 handleModRemoveShow(i: CommentNode) {
933 i.state.showRemoveDialog = true;
937 handleModRemoveReasonChange(i: CommentNode, event: any) {
938 i.state.removeReason = event.target.value;
942 handleModRemoveSubmit(i: CommentNode) {
943 event.preventDefault();
944 let form: CommentFormI = {
945 content: i.props.node.comment.content,
946 edit_id: i.props.node.comment.id,
947 creator_id: i.props.node.comment.creator_id,
948 post_id: i.props.node.comment.post_id,
949 parent_id: i.props.node.comment.parent_id,
950 removed: !i.props.node.comment.removed,
951 reason: i.state.removeReason,
954 WebSocketService.Instance.editComment(form);
956 i.state.showRemoveDialog = false;
960 handleMarkRead(i: CommentNode) {
961 // if it has a user_mention_id field, then its a mention
962 if (i.props.node.comment.user_mention_id) {
963 let form: EditUserMentionForm = {
964 user_mention_id: i.props.node.comment.user_mention_id,
965 read: !i.props.node.comment.read,
967 WebSocketService.Instance.editUserMention(form);
969 let form: CommentFormI = {
970 content: i.props.node.comment.content,
971 edit_id: i.props.node.comment.id,
972 creator_id: i.props.node.comment.creator_id,
973 post_id: i.props.node.comment.post_id,
974 parent_id: i.props.node.comment.parent_id,
975 read: !i.props.node.comment.read,
978 WebSocketService.Instance.editComment(form);
982 handleModBanFromCommunityShow(i: CommentNode) {
983 i.state.showBanDialog = !i.state.showBanDialog;
984 i.state.banType = BanType.Community;
988 handleModBanShow(i: CommentNode) {
989 i.state.showBanDialog = !i.state.showBanDialog;
990 i.state.banType = BanType.Site;
994 handleModBanReasonChange(i: CommentNode, event: any) {
995 i.state.banReason = event.target.value;
999 handleModBanExpiresChange(i: CommentNode, event: any) {
1000 i.state.banExpires = event.target.value;
1001 i.setState(i.state);
1004 handleModBanFromCommunitySubmit(i: CommentNode) {
1005 i.state.banType = BanType.Community;
1006 i.setState(i.state);
1007 i.handleModBanBothSubmit(i);
1010 handleModBanSubmit(i: CommentNode) {
1011 i.state.banType = BanType.Site;
1012 i.setState(i.state);
1013 i.handleModBanBothSubmit(i);
1016 handleModBanBothSubmit(i: CommentNode) {
1017 event.preventDefault();
1019 if (i.state.banType == BanType.Community) {
1020 let form: BanFromCommunityForm = {
1021 user_id: i.props.node.comment.creator_id,
1022 community_id: i.props.node.comment.community_id,
1023 ban: !i.props.node.comment.banned_from_community,
1024 reason: i.state.banReason,
1025 expires: getUnixTime(i.state.banExpires),
1027 WebSocketService.Instance.banFromCommunity(form);
1029 let form: BanUserForm = {
1030 user_id: i.props.node.comment.creator_id,
1031 ban: !i.props.node.comment.banned,
1032 reason: i.state.banReason,
1033 expires: getUnixTime(i.state.banExpires),
1035 WebSocketService.Instance.banUser(form);
1038 i.state.showBanDialog = false;
1039 i.setState(i.state);
1042 handleShowConfirmAppointAsMod(i: CommentNode) {
1043 i.state.showConfirmAppointAsMod = true;
1044 i.setState(i.state);
1047 handleCancelConfirmAppointAsMod(i: CommentNode) {
1048 i.state.showConfirmAppointAsMod = false;
1049 i.setState(i.state);
1052 handleAddModToCommunity(i: CommentNode) {
1053 let form: AddModToCommunityForm = {
1054 user_id: i.props.node.comment.creator_id,
1055 community_id: i.props.node.comment.community_id,
1058 WebSocketService.Instance.addModToCommunity(form);
1059 i.state.showConfirmAppointAsMod = false;
1060 i.setState(i.state);
1063 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1064 i.state.showConfirmAppointAsAdmin = true;
1065 i.setState(i.state);
1068 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1069 i.state.showConfirmAppointAsAdmin = false;
1070 i.setState(i.state);
1073 handleAddAdmin(i: CommentNode) {
1074 let form: AddAdminForm = {
1075 user_id: i.props.node.comment.creator_id,
1078 WebSocketService.Instance.addAdmin(form);
1079 i.state.showConfirmAppointAsAdmin = false;
1080 i.setState(i.state);
1083 handleShowConfirmTransferCommunity(i: CommentNode) {
1084 i.state.showConfirmTransferCommunity = true;
1085 i.setState(i.state);
1088 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1089 i.state.showConfirmTransferCommunity = false;
1090 i.setState(i.state);
1093 handleTransferCommunity(i: CommentNode) {
1094 let form: TransferCommunityForm = {
1095 community_id: i.props.node.comment.community_id,
1096 user_id: i.props.node.comment.creator_id,
1098 WebSocketService.Instance.transferCommunity(form);
1099 i.state.showConfirmTransferCommunity = false;
1100 i.setState(i.state);
1103 handleShowConfirmTransferSite(i: CommentNode) {
1104 i.state.showConfirmTransferSite = true;
1105 i.setState(i.state);
1108 handleCancelShowConfirmTransferSite(i: CommentNode) {
1109 i.state.showConfirmTransferSite = false;
1110 i.setState(i.state);
1113 handleTransferSite(i: CommentNode) {
1114 let form: TransferSiteForm = {
1115 user_id: i.props.node.comment.creator_id,
1117 WebSocketService.Instance.transferSite(form);
1118 i.state.showConfirmTransferSite = false;
1119 i.setState(i.state);
1122 get isCommentNew(): boolean {
1123 let now = moment.utc().subtract(10, 'minutes');
1124 let then = moment.utc(this.props.node.comment.published);
1125 return now.isBefore(then);
1128 handleCommentCollapse(i: CommentNode) {
1129 i.state.collapsed = !i.state.collapsed;
1130 i.setState(i.state);
1133 handleViewSource(i: CommentNode) {
1134 i.state.viewSource = !i.state.viewSource;
1135 i.setState(i.state);
1138 handleShowAdvanced(i: CommentNode) {
1139 i.state.showAdvanced = !i.state.showAdvanced;
1140 i.setState(i.state);
1145 if (this.state.my_vote == 1) {
1147 } else if (this.state.my_vote == -1) {
1148 return 'text-danger';
1150 return 'text-muted';