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,
30 import moment from 'moment';
31 import { MomentTime } from './moment-time';
32 import { CommentForm } from './comment-form';
33 import { CommentNodes } from './comment-nodes';
34 import { i18n } from '../i18next';
36 interface CommentNodeState {
39 showRemoveDialog: boolean;
41 showBanDialog: boolean;
45 showConfirmTransferSite: boolean;
46 showConfirmTransferCommunity: boolean;
47 showConfirmAppointAsMod: boolean;
48 showConfirmAppointAsAdmin: boolean;
57 interface CommentNodeProps {
63 moderators: Array<CommunityUser>;
64 admins: Array<UserView>;
65 // TODO is this necessary, can't I get it from the node itself?
66 postCreatorId?: number;
67 showCommunity?: boolean;
68 sort?: CommentSortType;
72 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
73 private emptyState: CommentNodeState = {
76 showRemoveDialog: false,
81 banType: BanType.Community,
84 showConfirmTransferSite: false,
85 showConfirmTransferCommunity: false,
86 showConfirmAppointAsMod: false,
87 showConfirmAppointAsAdmin: false,
88 my_vote: this.props.node.comment.my_vote,
89 score: this.props.node.comment.score,
90 upvotes: this.props.node.comment.upvotes,
91 downvotes: this.props.node.comment.downvotes,
94 constructor(props: any, context: any) {
95 super(props, context);
97 this.state = this.emptyState;
98 this.handleReplyCancel = this.handleReplyCancel.bind(this);
99 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
100 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
103 componentWillReceiveProps(nextProps: CommentNodeProps) {
104 this.state.my_vote = nextProps.node.comment.my_vote;
105 this.state.upvotes = nextProps.node.comment.upvotes;
106 this.state.downvotes = nextProps.node.comment.downvotes;
107 this.state.score = nextProps.node.comment.score;
108 this.setState(this.state);
112 let node = this.props.node;
115 className={`comment ${
116 node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
119 {!this.state.collapsed && (
121 className={`vote-bar mr-2 float-left small text-center ${this.props
122 .viewOnly && 'no-click'}`}
125 className={`vote-animate btn btn-link p-0 ${
126 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
128 onClick={linkEvent(node, this.handleCommentUpvote)}
130 <svg class="icon upvote">
131 <use xlinkHref="#icon-arrow-up"></use>
134 <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
135 {WebSocketService.Instance.site.enable_downvotes && (
137 className={`vote-animate btn btn-link p-0 ${
138 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
140 onClick={linkEvent(node, this.handleCommentDownvote)}
142 <svg class="icon downvote">
143 <use xlinkHref="#icon-arrow-down"></use>
150 id={`comment-${node.comment.id}`}
151 className={`details comment-node ml-4 ${
152 this.isCommentNew ? 'mark' : ''
155 <ul class="list-inline mb-0 text-muted small">
156 <li className="list-inline-item">
158 className="text-info"
159 to={`/u/${node.comment.creator_name}`}
161 {node.comment.creator_avatar && showAvatars() && (
165 src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
166 class="rounded-circle mr-1"
169 <span>{node.comment.creator_name}</span>
173 <li className="list-inline-item badge badge-light">
178 <li className="list-inline-item badge badge-light">
182 {this.isPostCreator && (
183 <li className="list-inline-item badge badge-light">
187 {(node.comment.banned_from_community || node.comment.banned) && (
188 <li className="list-inline-item badge badge-danger">
192 <li className="list-inline-item">
194 (<span className="text-info">+{this.state.upvotes}</span>
196 <span className="text-danger">-{this.state.downvotes}</span>
200 {this.props.showCommunity && (
201 <li className="list-inline-item">
202 <span> {i18n.t('to')} </span>
203 <Link to={`/c/${node.comment.community_name}`}>
204 {node.comment.community_name}
208 <li className="list-inline-item">
210 <MomentTime data={node.comment} />
213 <li className="list-inline-item">
215 className="pointer text-monospace"
216 onClick={linkEvent(this, this.handleCommentCollapse)}
218 {this.state.collapsed ? '[+]' : '[-]'}
222 {this.state.showEdit && (
226 onReplyCancel={this.handleReplyCancel}
227 disabled={this.props.locked}
230 {!this.state.showEdit && !this.state.collapsed && (
232 {this.state.viewSource ? (
233 <pre>{this.commentUnlessRemoved}</pre>
237 dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
240 <ul class="list-inline mb-1 text-muted small font-weight-bold">
241 {this.props.markable && (
242 <li className="list-inline-item">
245 onClick={linkEvent(this, this.handleMarkRead)}
248 ? i18n.t('mark_as_unread')
249 : i18n.t('mark_as_read')}
253 {UserService.Instance.user && !this.props.viewOnly && (
255 <li className="list-inline-item">
258 onClick={linkEvent(this, this.handleReplyClick)}
263 <li className="list-inline-item mr-2">
266 onClick={linkEvent(this, this.handleSaveCommentClick)}
268 {node.comment.saved ? i18n.t('unsave') : i18n.t('save')}
273 <li className="list-inline-item">
276 onClick={linkEvent(this, this.handleEditClick)}
281 <li className="list-inline-item">
284 onClick={linkEvent(this, this.handleDeleteClick)}
286 {!node.comment.deleted
293 {!this.myComment && (
294 <li className="list-inline-item">
297 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
299 {i18n.t('message').toLowerCase()}
303 <li className="list-inline-item">•</li>
304 <li className="list-inline-item">
307 onClick={linkEvent(this, this.handleViewSource)}
309 {i18n.t('view_source')}
312 <li className="list-inline-item">
314 className="text-muted"
315 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
320 {/* Admins and mods can remove comments */}
321 {(this.canMod || this.canAdmin) && (
323 <li className="list-inline-item">•</li>
324 <li className="list-inline-item">
325 {!node.comment.removed ? (
330 this.handleModRemoveShow
340 this.handleModRemoveSubmit
349 {/* Mods can ban from community, and appoint as mods to community */}
353 <li className="list-inline-item">
354 {!node.comment.banned_from_community ? (
359 this.handleModBanFromCommunityShow
369 this.handleModBanFromCommunitySubmit
377 {!node.comment.banned_from_community && (
378 <li className="list-inline-item">
379 {!this.state.showConfirmAppointAsMod ? (
384 this.handleShowConfirmAppointAsMod
388 ? i18n.t('remove_as_mod')
389 : i18n.t('appoint_as_mod')}
393 <span class="d-inline-block mr-1">
394 {i18n.t('are_you_sure')}
397 class="pointer d-inline-block mr-1"
400 this.handleAddModToCommunity
406 class="pointer d-inline-block"
409 this.handleCancelConfirmAppointAsMod
420 {/* Community creators and admins can transfer community to another mod */}
421 {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
422 <li className="list-inline-item">
423 {!this.state.showConfirmTransferCommunity ? (
428 this.handleShowConfirmTransferCommunity
431 {i18n.t('transfer_community')}
435 <span class="d-inline-block mr-1">
436 {i18n.t('are_you_sure')}
439 class="pointer d-inline-block mr-1"
442 this.handleTransferCommunity
448 class="pointer d-inline-block"
451 this.handleCancelShowConfirmTransferCommunity
460 {/* Admins can ban from all, and appoint other admins */}
464 <li className="list-inline-item">
465 {!node.comment.banned ? (
468 onClick={linkEvent(this, this.handleModBanShow)}
470 {i18n.t('ban_from_site')}
477 this.handleModBanSubmit
480 {i18n.t('unban_from_site')}
485 {!node.comment.banned && (
486 <li className="list-inline-item">
487 {!this.state.showConfirmAppointAsAdmin ? (
492 this.handleShowConfirmAppointAsAdmin
496 ? i18n.t('remove_as_admin')
497 : i18n.t('appoint_as_admin')}
501 <span class="d-inline-block mr-1">
502 {i18n.t('are_you_sure')}
505 class="pointer d-inline-block mr-1"
506 onClick={linkEvent(this, this.handleAddAdmin)}
511 class="pointer d-inline-block"
514 this.handleCancelConfirmAppointAsAdmin
525 {/* Site Creator can transfer to another admin */}
526 {this.amSiteCreator && this.isAdmin && (
527 <li className="list-inline-item">
528 {!this.state.showConfirmTransferSite ? (
533 this.handleShowConfirmTransferSite
536 {i18n.t('transfer_site')}
540 <span class="d-inline-block mr-1">
541 {i18n.t('are_you_sure')}
544 class="pointer d-inline-block mr-1"
545 onClick={linkEvent(this, this.handleTransferSite)}
550 class="pointer d-inline-block"
553 this.handleCancelShowConfirmTransferSite
568 {this.state.showRemoveDialog && (
571 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
575 class="form-control mr-2"
576 placeholder={i18n.t('reason')}
577 value={this.state.removeReason}
578 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
580 <button type="submit" class="btn btn-secondary">
581 {i18n.t('remove_comment')}
585 {this.state.showBanDialog && (
586 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
587 <div class="form-group row">
588 <label class="col-form-label">{i18n.t('reason')}</label>
591 class="form-control mr-2"
592 placeholder={i18n.t('reason')}
593 value={this.state.banReason}
594 onInput={linkEvent(this, this.handleModBanReasonChange)}
597 {/* TODO hold off on expires until later */}
598 {/* <div class="form-group row"> */}
599 {/* <label class="col-form-label">Expires</label> */}
600 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
602 <div class="form-group row">
603 <button type="submit" class="btn btn-secondary">
604 {i18n.t('ban')} {node.comment.creator_name}
609 {this.state.showReply && (
612 onReplyCancel={this.handleReplyCancel}
613 disabled={this.props.locked}
616 {node.children && !this.state.collapsed && (
618 nodes={node.children}
619 locked={this.props.locked}
620 moderators={this.props.moderators}
621 admins={this.props.admins}
622 postCreatorId={this.props.postCreatorId}
623 sort={this.props.sort}
624 sortType={this.props.sortType}
627 {/* A collapsed clearfix */}
628 {this.state.collapsed && <div class="row col-12"></div>}
633 get myComment(): boolean {
635 UserService.Instance.user &&
636 this.props.node.comment.creator_id == UserService.Instance.user.id
640 get isMod(): boolean {
642 this.props.moderators &&
644 this.props.moderators.map(m => m.user_id),
645 this.props.node.comment.creator_id
650 get isAdmin(): boolean {
654 this.props.admins.map(a => a.id),
655 this.props.node.comment.creator_id
660 get isPostCreator(): boolean {
661 return this.props.node.comment.creator_id == this.props.postCreatorId;
664 get canMod(): boolean {
665 if (this.props.admins && this.props.moderators) {
666 let adminsThenMods = this.props.admins
668 .concat(this.props.moderators.map(m => m.user_id));
671 UserService.Instance.user,
673 this.props.node.comment.creator_id
680 get canAdmin(): boolean {
684 UserService.Instance.user,
685 this.props.admins.map(a => a.id),
686 this.props.node.comment.creator_id
691 get amCommunityCreator(): boolean {
693 this.props.moderators &&
694 UserService.Instance.user &&
695 this.props.node.comment.creator_id != UserService.Instance.user.id &&
696 UserService.Instance.user.id == this.props.moderators[0].user_id
700 get amSiteCreator(): boolean {
703 UserService.Instance.user &&
704 this.props.node.comment.creator_id != UserService.Instance.user.id &&
705 UserService.Instance.user.id == this.props.admins[0].id
709 get commentUnlessRemoved(): string {
710 let node = this.props.node;
711 return node.comment.removed
712 ? `*${i18n.t('removed')}*`
713 : node.comment.deleted
714 ? `*${i18n.t('deleted')}*`
715 : node.comment.content;
718 handleReplyClick(i: CommentNode) {
719 i.state.showReply = true;
723 handleEditClick(i: CommentNode) {
724 i.state.showEdit = true;
728 handleDeleteClick(i: CommentNode) {
729 let deleteForm: CommentFormI = {
730 content: i.props.node.comment.content,
731 edit_id: i.props.node.comment.id,
732 creator_id: i.props.node.comment.creator_id,
733 post_id: i.props.node.comment.post_id,
734 parent_id: i.props.node.comment.parent_id,
735 deleted: !i.props.node.comment.deleted,
738 WebSocketService.Instance.editComment(deleteForm);
741 handleSaveCommentClick(i: CommentNode) {
743 i.props.node.comment.saved == undefined
745 : !i.props.node.comment.saved;
746 let form: SaveCommentForm = {
747 comment_id: i.props.node.comment.id,
751 WebSocketService.Instance.saveComment(form);
754 handleReplyCancel() {
755 this.state.showReply = false;
756 this.state.showEdit = false;
757 this.setState(this.state);
760 handleCommentUpvote(i: CommentNodeI) {
761 let new_vote = this.state.my_vote == 1 ? 0 : 1;
763 if (this.state.my_vote == 1) {
765 this.state.upvotes--;
766 } else if (this.state.my_vote == -1) {
767 this.state.downvotes--;
768 this.state.upvotes++;
769 this.state.score += 2;
771 this.state.upvotes++;
775 this.state.my_vote = new_vote;
777 let form: CommentLikeForm = {
778 comment_id: i.comment.id,
779 post_id: i.comment.post_id,
780 score: this.state.my_vote,
783 WebSocketService.Instance.likeComment(form);
784 this.setState(this.state);
787 handleCommentDownvote(i: CommentNodeI) {
788 let new_vote = this.state.my_vote == -1 ? 0 : -1;
790 if (this.state.my_vote == 1) {
791 this.state.score -= 2;
792 this.state.upvotes--;
793 this.state.downvotes++;
794 } else if (this.state.my_vote == -1) {
795 this.state.downvotes--;
798 this.state.downvotes++;
802 this.state.my_vote = new_vote;
804 let form: CommentLikeForm = {
805 comment_id: i.comment.id,
806 post_id: i.comment.post_id,
807 score: this.state.my_vote,
810 WebSocketService.Instance.likeComment(form);
811 this.setState(this.state);
814 handleModRemoveShow(i: CommentNode) {
815 i.state.showRemoveDialog = true;
819 handleModRemoveReasonChange(i: CommentNode, event: any) {
820 i.state.removeReason = event.target.value;
824 handleModRemoveSubmit(i: CommentNode) {
825 event.preventDefault();
826 let form: CommentFormI = {
827 content: i.props.node.comment.content,
828 edit_id: i.props.node.comment.id,
829 creator_id: i.props.node.comment.creator_id,
830 post_id: i.props.node.comment.post_id,
831 parent_id: i.props.node.comment.parent_id,
832 removed: !i.props.node.comment.removed,
833 reason: i.state.removeReason,
836 WebSocketService.Instance.editComment(form);
838 i.state.showRemoveDialog = false;
842 handleMarkRead(i: CommentNode) {
843 // if it has a user_mention_id field, then its a mention
844 if (i.props.node.comment.user_mention_id) {
845 let form: EditUserMentionForm = {
846 user_mention_id: i.props.node.comment.user_mention_id,
847 read: !i.props.node.comment.read,
849 WebSocketService.Instance.editUserMention(form);
851 let form: 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 read: !i.props.node.comment.read,
860 WebSocketService.Instance.editComment(form);
864 handleModBanFromCommunityShow(i: CommentNode) {
865 i.state.showBanDialog = !i.state.showBanDialog;
866 i.state.banType = BanType.Community;
870 handleModBanShow(i: CommentNode) {
871 i.state.showBanDialog = !i.state.showBanDialog;
872 i.state.banType = BanType.Site;
876 handleModBanReasonChange(i: CommentNode, event: any) {
877 i.state.banReason = event.target.value;
881 handleModBanExpiresChange(i: CommentNode, event: any) {
882 i.state.banExpires = event.target.value;
886 handleModBanFromCommunitySubmit(i: CommentNode) {
887 i.state.banType = BanType.Community;
889 i.handleModBanBothSubmit(i);
892 handleModBanSubmit(i: CommentNode) {
893 i.state.banType = BanType.Site;
895 i.handleModBanBothSubmit(i);
898 handleModBanBothSubmit(i: CommentNode) {
899 event.preventDefault();
901 if (i.state.banType == BanType.Community) {
902 let form: BanFromCommunityForm = {
903 user_id: i.props.node.comment.creator_id,
904 community_id: i.props.node.comment.community_id,
905 ban: !i.props.node.comment.banned_from_community,
906 reason: i.state.banReason,
907 expires: getUnixTime(i.state.banExpires),
909 WebSocketService.Instance.banFromCommunity(form);
911 let form: BanUserForm = {
912 user_id: i.props.node.comment.creator_id,
913 ban: !i.props.node.comment.banned,
914 reason: i.state.banReason,
915 expires: getUnixTime(i.state.banExpires),
917 WebSocketService.Instance.banUser(form);
920 i.state.showBanDialog = false;
924 handleShowConfirmAppointAsMod(i: CommentNode) {
925 i.state.showConfirmAppointAsMod = true;
929 handleCancelConfirmAppointAsMod(i: CommentNode) {
930 i.state.showConfirmAppointAsMod = false;
934 handleAddModToCommunity(i: CommentNode) {
935 let form: AddModToCommunityForm = {
936 user_id: i.props.node.comment.creator_id,
937 community_id: i.props.node.comment.community_id,
940 WebSocketService.Instance.addModToCommunity(form);
941 i.state.showConfirmAppointAsMod = false;
945 handleShowConfirmAppointAsAdmin(i: CommentNode) {
946 i.state.showConfirmAppointAsAdmin = true;
950 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
951 i.state.showConfirmAppointAsAdmin = false;
955 handleAddAdmin(i: CommentNode) {
956 let form: AddAdminForm = {
957 user_id: i.props.node.comment.creator_id,
960 WebSocketService.Instance.addAdmin(form);
961 i.state.showConfirmAppointAsAdmin = false;
965 handleShowConfirmTransferCommunity(i: CommentNode) {
966 i.state.showConfirmTransferCommunity = true;
970 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
971 i.state.showConfirmTransferCommunity = false;
975 handleTransferCommunity(i: CommentNode) {
976 let form: TransferCommunityForm = {
977 community_id: i.props.node.comment.community_id,
978 user_id: i.props.node.comment.creator_id,
980 WebSocketService.Instance.transferCommunity(form);
981 i.state.showConfirmTransferCommunity = false;
985 handleShowConfirmTransferSite(i: CommentNode) {
986 i.state.showConfirmTransferSite = true;
990 handleCancelShowConfirmTransferSite(i: CommentNode) {
991 i.state.showConfirmTransferSite = false;
995 handleTransferSite(i: CommentNode) {
996 let form: TransferSiteForm = {
997 user_id: i.props.node.comment.creator_id,
999 WebSocketService.Instance.transferSite(form);
1000 i.state.showConfirmTransferSite = false;
1001 i.setState(i.state);
1004 get isCommentNew(): boolean {
1005 let now = moment.utc().subtract(10, 'minutes');
1006 let then = moment.utc(this.props.node.comment.published);
1007 return now.isBefore(then);
1010 handleCommentCollapse(i: CommentNode) {
1011 i.state.collapsed = !i.state.collapsed;
1012 i.setState(i.state);
1015 handleViewSource(i: CommentNode) {
1016 i.state.viewSource = !i.state.viewSource;
1017 i.setState(i.state);