1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
4 CommentNode as CommentNodeI,
9 MarkUserMentionAsReadForm,
15 AddModToCommunityForm,
17 TransferCommunityForm,
22 } from '../interfaces';
23 import { WebSocketService, UserService } from '../services';
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 { UserListing } from './user-listing';
37 import { CommunityLink } from './community-link';
38 import { i18n } from '../i18next';
40 interface CommentNodeState {
43 showRemoveDialog: boolean;
45 showBanDialog: boolean;
50 showConfirmTransferSite: boolean;
51 showConfirmTransferCommunity: boolean;
52 showConfirmAppointAsMod: boolean;
53 showConfirmAppointAsAdmin: boolean;
56 showAdvanced: boolean;
66 interface CommentNodeProps {
73 showContext?: boolean;
74 moderators: Array<CommunityUser>;
75 admins: Array<UserView>;
76 // TODO is this necessary, can't I get it from the node itself?
77 postCreatorId?: number;
78 showCommunity?: boolean;
79 sort?: CommentSortType;
81 enableDownvotes: boolean;
84 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
85 private emptyState: CommentNodeState = {
88 showRemoveDialog: false,
94 banType: BanType.Community,
98 showConfirmTransferSite: false,
99 showConfirmTransferCommunity: false,
100 showConfirmAppointAsMod: false,
101 showConfirmAppointAsAdmin: false,
102 my_vote: this.props.node.comment.my_vote,
103 score: this.props.node.comment.score,
104 upvotes: this.props.node.comment.upvotes,
105 downvotes: this.props.node.comment.downvotes,
106 borderColor: this.props.node.comment.depth
107 ? colorList[this.props.node.comment.depth % colorList.length]
113 constructor(props: any, context: any) {
114 super(props, context);
116 this.state = this.emptyState;
117 this.handleReplyCancel = this.handleReplyCancel.bind(this);
118 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
119 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
122 componentWillReceiveProps(nextProps: CommentNodeProps) {
123 this.state.my_vote = nextProps.node.comment.my_vote;
124 this.state.upvotes = nextProps.node.comment.upvotes;
125 this.state.downvotes = nextProps.node.comment.downvotes;
126 this.state.score = nextProps.node.comment.score;
127 this.state.readLoading = false;
128 this.state.saveLoading = false;
129 this.setState(this.state);
133 let node = this.props.node;
136 className={`comment ${
137 node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
141 id={`comment-${node.comment.id}`}
142 className={`details comment-node py-2 ${
143 !this.props.noBorder ? 'border-top border-light' : ''
144 } ${this.isCommentNew ? 'mark' : ''}`}
146 !this.props.noIndent &&
147 this.props.node.comment.parent_id &&
148 `border-left: 2px ${this.state.borderColor} solid !important`
153 !this.props.noIndent &&
154 this.props.node.comment.parent_id &&
158 <div class="d-flex flex-wrap align-items-center text-muted small">
162 name: node.comment.creator_name,
163 preferred_username: node.comment.creator_preferred_username,
164 avatar: node.comment.creator_avatar,
165 id: node.comment.creator_id,
166 local: node.comment.creator_local,
167 actor_id: node.comment.creator_actor_id,
168 published: node.comment.creator_published,
174 <div className="badge badge-light d-none d-sm-inline mr-2">
179 <div className="badge badge-light d-none d-sm-inline mr-2">
183 {this.isPostCreator && (
184 <div className="badge badge-light d-none d-sm-inline mr-2">
188 {(node.comment.banned_from_community || node.comment.banned) && (
189 <div className="badge badge-danger mr-2">
193 {this.props.showCommunity && (
195 <span class="mx-1">{i18n.t('to')}</span>
198 name: node.comment.community_name,
199 id: node.comment.community_id,
200 local: node.comment.community_local,
201 actor_id: node.comment.community_actor_id,
202 icon: node.comment.community_icon,
205 <span class="mx-2">•</span>
206 <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
207 {node.comment.post_name}
212 class="btn text-muted"
213 onClick={linkEvent(this, this.handleCommentCollapse)}
215 {this.state.collapsed ? (
216 <svg class="icon icon-inline">
217 <use xlinkHref="#icon-plus-square"></use>
220 <svg class="icon icon-inline">
221 <use xlinkHref="#icon-minus-square"></use>
225 {/* This is an expanding spacer for mobile */}
226 <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
228 className={`btn p-0 unselectable pointer ${this.scoreColor}`}
229 onClick={linkEvent(node, this.handleCommentUpvote)}
230 data-tippy-content={this.pointsTippy}
232 <svg class="icon icon-inline mr-1">
233 <use xlinkHref="#icon-zap"></use>
235 <span class="mr-1">{this.state.score}</span>
237 <span className="mr-1">•</span>
239 <MomentTime data={node.comment} />
242 {/* end of user row */}
243 {this.state.showEdit && (
247 onReplyCancel={this.handleReplyCancel}
248 disabled={this.props.locked}
252 {!this.state.showEdit && !this.state.collapsed && (
254 {this.state.viewSource ? (
255 <pre>{this.commentUnlessRemoved}</pre>
259 dangerouslySetInnerHTML={mdToHtml(
260 this.commentUnlessRemoved
264 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
265 {this.props.showContext && this.linkBtn}
266 {this.props.markable && (
268 class="btn btn-link btn-animate text-muted"
269 onClick={linkEvent(this, this.handleMarkRead)}
272 ? i18n.t('mark_as_unread')
273 : i18n.t('mark_as_read')
276 {this.state.readLoading ? (
280 class={`icon icon-inline ${
281 node.comment.read && 'text-success'
284 <use xlinkHref="#icon-check"></use>
289 {UserService.Instance.user && !this.props.viewOnly && (
292 className={`btn btn-link btn-animate ${
293 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
295 onClick={linkEvent(node, this.handleCommentUpvote)}
296 data-tippy-content={i18n.t('upvote')}
298 <svg class="icon icon-inline">
299 <use xlinkHref="#icon-arrow-up"></use>
301 {this.state.upvotes !== this.state.score && (
302 <span class="ml-1">{this.state.upvotes}</span>
305 {this.props.enableDownvotes && (
307 className={`btn btn-link btn-animate ${
308 this.state.my_vote == -1
312 onClick={linkEvent(node, this.handleCommentDownvote)}
313 data-tippy-content={i18n.t('downvote')}
315 <svg class="icon icon-inline">
316 <use xlinkHref="#icon-arrow-down"></use>
318 {this.state.upvotes !== this.state.score && (
319 <span class="ml-1">{this.state.downvotes}</span>
324 class="btn btn-link btn-animate text-muted"
325 onClick={linkEvent(this, this.handleReplyClick)}
326 data-tippy-content={i18n.t('reply')}
328 <svg class="icon icon-inline">
329 <use xlinkHref="#icon-reply1"></use>
332 {!this.state.showAdvanced ? (
334 className="btn btn-link btn-animate text-muted"
335 onClick={linkEvent(this, this.handleShowAdvanced)}
336 data-tippy-content={i18n.t('more')}
338 <svg class="icon icon-inline">
339 <use xlinkHref="#icon-more-vertical"></use>
344 {!this.myComment && (
345 <button class="btn btn-link btn-animate">
348 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
349 title={i18n.t('message').toLowerCase()}
352 <use xlinkHref="#icon-mail"></use>
357 {!this.props.showContext && this.linkBtn}
359 class="btn btn-link btn-animate text-muted"
362 this.handleSaveCommentClick
370 {this.state.saveLoading ? (
374 class={`icon icon-inline ${
375 node.comment.saved && 'text-warning'
378 <use xlinkHref="#icon-star"></use>
383 className="btn btn-link btn-animate text-muted"
384 onClick={linkEvent(this, this.handleViewSource)}
385 data-tippy-content={i18n.t('view_source')}
388 class={`icon icon-inline ${
389 this.state.viewSource && 'text-success'
392 <use xlinkHref="#icon-file-text"></use>
398 class="btn btn-link btn-animate text-muted"
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 class="btn btn-link btn-animate text-muted"
410 this.handleDeleteClick
413 !node.comment.deleted
419 class={`icon icon-inline ${
420 node.comment.deleted && 'text-danger'
423 <use xlinkHref="#icon-trash"></use>
428 {/* Admins and mods can remove comments */}
429 {(this.canMod || this.canAdmin) && (
431 {!node.comment.removed ? (
433 class="btn btn-link btn-animate text-muted"
436 this.handleModRemoveShow
443 class="btn btn-link btn-animate text-muted"
446 this.handleModRemoveSubmit
454 {/* Mods can ban from community, and appoint as mods to community */}
458 (!node.comment.banned_from_community ? (
460 class="btn btn-link btn-animate text-muted"
463 this.handleModBanFromCommunityShow
470 class="btn btn-link btn-animate text-muted"
473 this.handleModBanFromCommunitySubmit
479 {!node.comment.banned_from_community &&
480 node.comment.creator_local &&
481 (!this.state.showConfirmAppointAsMod ? (
483 class="btn btn-link btn-animate text-muted"
486 this.handleShowConfirmAppointAsMod
490 ? i18n.t('remove_as_mod')
491 : i18n.t('appoint_as_mod')}
495 <button class="btn btn-link btn-animate text-muted">
496 {i18n.t('are_you_sure')}
499 class="btn btn-link btn-animate text-muted"
502 this.handleAddModToCommunity
508 class="btn btn-link btn-animate text-muted"
511 this.handleCancelConfirmAppointAsMod
520 {/* Community creators and admins can transfer community to another mod */}
521 {(this.amCommunityCreator || this.canAdmin) &&
523 node.comment.creator_local &&
524 (!this.state.showConfirmTransferCommunity ? (
526 class="btn btn-link btn-animate text-muted"
529 this.handleShowConfirmTransferCommunity
532 {i18n.t('transfer_community')}
536 <button class="btn btn-link btn-animate text-muted">
537 {i18n.t('are_you_sure')}
540 class="btn btn-link btn-animate text-muted"
543 this.handleTransferCommunity
549 class="btn btn-link btn-animate text-muted"
553 .handleCancelShowConfirmTransferCommunity
560 {/* Admins can ban from all, and appoint other admins */}
564 (!node.comment.banned ? (
566 class="btn btn-link btn-animate text-muted"
569 this.handleModBanShow
572 {i18n.t('ban_from_site')}
576 class="btn btn-link btn-animate text-muted"
579 this.handleModBanSubmit
582 {i18n.t('unban_from_site')}
585 {!node.comment.banned &&
586 node.comment.creator_local &&
587 (!this.state.showConfirmAppointAsAdmin ? (
589 class="btn btn-link btn-animate text-muted"
592 this.handleShowConfirmAppointAsAdmin
596 ? i18n.t('remove_as_admin')
597 : i18n.t('appoint_as_admin')}
601 <button class="btn btn-link btn-animate text-muted">
602 {i18n.t('are_you_sure')}
605 class="btn btn-link btn-animate text-muted"
614 class="btn btn-link btn-animate text-muted"
617 this.handleCancelConfirmAppointAsAdmin
626 {/* Site Creator can transfer to another admin */}
627 {this.amSiteCreator &&
629 node.comment.creator_local &&
630 (!this.state.showConfirmTransferSite ? (
632 class="btn btn-link btn-animate text-muted"
635 this.handleShowConfirmTransferSite
638 {i18n.t('transfer_site')}
642 <button class="btn btn-link btn-animate text-muted">
643 {i18n.t('are_you_sure')}
646 class="btn btn-link btn-animate text-muted"
649 this.handleTransferSite
655 class="btn btn-link btn-animate text-muted"
658 this.handleCancelShowConfirmTransferSite
670 {/* end of button group */}
675 {/* end of details */}
676 {this.state.showRemoveDialog && (
679 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
683 class="form-control mr-2"
684 placeholder={i18n.t('reason')}
685 value={this.state.removeReason}
686 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
688 <button type="submit" class="btn btn-secondary">
689 {i18n.t('remove_comment')}
693 {this.state.showBanDialog && (
694 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
695 <div class="form-group row">
696 <label class="col-form-label">{i18n.t('reason')}</label>
699 class="form-control mr-2"
700 placeholder={i18n.t('reason')}
701 value={this.state.banReason}
702 onInput={linkEvent(this, this.handleModBanReasonChange)}
704 <div class="form-group">
705 <div class="form-check">
707 class="form-check-input"
708 id="mod-ban-remove-data"
710 checked={this.state.removeData}
711 onChange={linkEvent(this, this.handleModRemoveDataChange)}
713 <label class="form-check-label" htmlFor="mod-ban-remove-data">
714 {i18n.t('remove_posts_comments')}
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}
739 {node.children && !this.state.collapsed && (
741 nodes={node.children}
742 locked={this.props.locked}
743 moderators={this.props.moderators}
744 admins={this.props.admins}
745 postCreatorId={this.props.postCreatorId}
746 sort={this.props.sort}
747 sortType={this.props.sortType}
748 enableDownvotes={this.props.enableDownvotes}
751 {/* A collapsed clearfix */}
752 {this.state.collapsed && <div class="row col-12"></div>}
758 let node = this.props.node;
761 class="btn btn-link btn-animate text-muted"
762 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
763 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
765 <svg class="icon icon-inline">
766 <use xlinkHref="#icon-link"></use>
774 <svg class="icon icon-spinner spin">
775 <use xlinkHref="#icon-spinner"></use>
780 get myComment(): boolean {
782 UserService.Instance.user &&
783 this.props.node.comment.creator_id == UserService.Instance.user.id
787 get isMod(): boolean {
789 this.props.moderators &&
791 this.props.moderators.map(m => m.user_id),
792 this.props.node.comment.creator_id
797 get isAdmin(): boolean {
801 this.props.admins.map(a => a.id),
802 this.props.node.comment.creator_id
807 get isPostCreator(): boolean {
808 return this.props.node.comment.creator_id == this.props.postCreatorId;
811 get canMod(): boolean {
812 if (this.props.admins && this.props.moderators) {
813 let adminsThenMods = this.props.admins
815 .concat(this.props.moderators.map(m => m.user_id));
818 UserService.Instance.user,
820 this.props.node.comment.creator_id
827 get canAdmin(): boolean {
831 UserService.Instance.user,
832 this.props.admins.map(a => a.id),
833 this.props.node.comment.creator_id
838 get amCommunityCreator(): boolean {
840 this.props.moderators &&
841 UserService.Instance.user &&
842 this.props.node.comment.creator_id != UserService.Instance.user.id &&
843 UserService.Instance.user.id == this.props.moderators[0].user_id
847 get amSiteCreator(): boolean {
850 UserService.Instance.user &&
851 this.props.node.comment.creator_id != UserService.Instance.user.id &&
852 UserService.Instance.user.id == this.props.admins[0].id
856 get commentUnlessRemoved(): string {
857 let node = this.props.node;
858 return node.comment.removed
859 ? `*${i18n.t('removed')}*`
860 : node.comment.deleted
861 ? `*${i18n.t('deleted')}*`
862 : node.comment.content;
865 handleReplyClick(i: CommentNode) {
866 i.state.showReply = true;
870 handleEditClick(i: CommentNode) {
871 i.state.showEdit = true;
875 handleDeleteClick(i: CommentNode) {
876 let deleteForm: DeleteCommentForm = {
877 edit_id: i.props.node.comment.id,
878 deleted: !i.props.node.comment.deleted,
881 WebSocketService.Instance.deleteComment(deleteForm);
884 handleSaveCommentClick(i: CommentNode) {
886 i.props.node.comment.saved == undefined
888 : !i.props.node.comment.saved;
889 let form: SaveCommentForm = {
890 comment_id: i.props.node.comment.id,
894 WebSocketService.Instance.saveComment(form);
896 i.state.saveLoading = true;
897 i.setState(this.state);
900 handleReplyCancel() {
901 this.state.showReply = false;
902 this.state.showEdit = false;
903 this.setState(this.state);
906 handleCommentUpvote(i: CommentNodeI) {
907 let new_vote = this.state.my_vote == 1 ? 0 : 1;
909 if (this.state.my_vote == 1) {
911 this.state.upvotes--;
912 } else if (this.state.my_vote == -1) {
913 this.state.downvotes--;
914 this.state.upvotes++;
915 this.state.score += 2;
917 this.state.upvotes++;
921 this.state.my_vote = new_vote;
923 let form: CommentLikeForm = {
924 comment_id: i.comment.id,
925 score: this.state.my_vote,
928 WebSocketService.Instance.likeComment(form);
929 this.setState(this.state);
933 handleCommentDownvote(i: CommentNodeI) {
934 let new_vote = this.state.my_vote == -1 ? 0 : -1;
936 if (this.state.my_vote == 1) {
937 this.state.score -= 2;
938 this.state.upvotes--;
939 this.state.downvotes++;
940 } else if (this.state.my_vote == -1) {
941 this.state.downvotes--;
944 this.state.downvotes++;
948 this.state.my_vote = new_vote;
950 let form: CommentLikeForm = {
951 comment_id: i.comment.id,
952 score: this.state.my_vote,
955 WebSocketService.Instance.likeComment(form);
956 this.setState(this.state);
960 handleModRemoveShow(i: CommentNode) {
961 i.state.showRemoveDialog = true;
965 handleModRemoveReasonChange(i: CommentNode, event: any) {
966 i.state.removeReason = event.target.value;
970 handleModRemoveDataChange(i: CommentNode, event: any) {
971 i.state.removeData = event.target.checked;
975 handleModRemoveSubmit(i: CommentNode) {
976 event.preventDefault();
977 let form: RemoveCommentForm = {
978 edit_id: i.props.node.comment.id,
979 removed: !i.props.node.comment.removed,
980 reason: i.state.removeReason,
983 WebSocketService.Instance.removeComment(form);
985 i.state.showRemoveDialog = false;
989 handleMarkRead(i: CommentNode) {
990 // if it has a user_mention_id field, then its a mention
991 if (i.props.node.comment.user_mention_id) {
992 let form: MarkUserMentionAsReadForm = {
993 user_mention_id: i.props.node.comment.user_mention_id,
994 read: !i.props.node.comment.read,
996 WebSocketService.Instance.markUserMentionAsRead(form);
998 let form: MarkCommentAsReadForm = {
999 edit_id: i.props.node.comment.id,
1000 read: !i.props.node.comment.read,
1003 WebSocketService.Instance.markCommentAsRead(form);
1006 i.state.readLoading = true;
1007 i.setState(this.state);
1010 handleModBanFromCommunityShow(i: CommentNode) {
1011 i.state.showBanDialog = !i.state.showBanDialog;
1012 i.state.banType = BanType.Community;
1013 i.setState(i.state);
1016 handleModBanShow(i: CommentNode) {
1017 i.state.showBanDialog = !i.state.showBanDialog;
1018 i.state.banType = BanType.Site;
1019 i.setState(i.state);
1022 handleModBanReasonChange(i: CommentNode, event: any) {
1023 i.state.banReason = event.target.value;
1024 i.setState(i.state);
1027 handleModBanExpiresChange(i: CommentNode, event: any) {
1028 i.state.banExpires = event.target.value;
1029 i.setState(i.state);
1032 handleModBanFromCommunitySubmit(i: CommentNode) {
1033 i.state.banType = BanType.Community;
1034 i.setState(i.state);
1035 i.handleModBanBothSubmit(i);
1038 handleModBanSubmit(i: CommentNode) {
1039 i.state.banType = BanType.Site;
1040 i.setState(i.state);
1041 i.handleModBanBothSubmit(i);
1044 handleModBanBothSubmit(i: CommentNode) {
1045 event.preventDefault();
1047 if (i.state.banType == BanType.Community) {
1048 // If its an unban, restore all their data
1049 let ban = !i.props.node.comment.banned_from_community;
1051 i.state.removeData = false;
1053 let form: BanFromCommunityForm = {
1054 user_id: i.props.node.comment.creator_id,
1055 community_id: i.props.node.comment.community_id,
1057 remove_data: i.state.removeData,
1058 reason: i.state.banReason,
1059 expires: getUnixTime(i.state.banExpires),
1061 WebSocketService.Instance.banFromCommunity(form);
1063 // If its an unban, restore all their data
1064 let ban = !i.props.node.comment.banned;
1066 i.state.removeData = false;
1068 let form: BanUserForm = {
1069 user_id: i.props.node.comment.creator_id,
1071 remove_data: i.state.removeData,
1072 reason: i.state.banReason,
1073 expires: getUnixTime(i.state.banExpires),
1075 WebSocketService.Instance.banUser(form);
1078 i.state.showBanDialog = false;
1079 i.setState(i.state);
1082 handleShowConfirmAppointAsMod(i: CommentNode) {
1083 i.state.showConfirmAppointAsMod = true;
1084 i.setState(i.state);
1087 handleCancelConfirmAppointAsMod(i: CommentNode) {
1088 i.state.showConfirmAppointAsMod = false;
1089 i.setState(i.state);
1092 handleAddModToCommunity(i: CommentNode) {
1093 let form: AddModToCommunityForm = {
1094 user_id: i.props.node.comment.creator_id,
1095 community_id: i.props.node.comment.community_id,
1098 WebSocketService.Instance.addModToCommunity(form);
1099 i.state.showConfirmAppointAsMod = false;
1100 i.setState(i.state);
1103 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1104 i.state.showConfirmAppointAsAdmin = true;
1105 i.setState(i.state);
1108 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1109 i.state.showConfirmAppointAsAdmin = false;
1110 i.setState(i.state);
1113 handleAddAdmin(i: CommentNode) {
1114 let form: AddAdminForm = {
1115 user_id: i.props.node.comment.creator_id,
1118 WebSocketService.Instance.addAdmin(form);
1119 i.state.showConfirmAppointAsAdmin = false;
1120 i.setState(i.state);
1123 handleShowConfirmTransferCommunity(i: CommentNode) {
1124 i.state.showConfirmTransferCommunity = true;
1125 i.setState(i.state);
1128 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1129 i.state.showConfirmTransferCommunity = false;
1130 i.setState(i.state);
1133 handleTransferCommunity(i: CommentNode) {
1134 let form: TransferCommunityForm = {
1135 community_id: i.props.node.comment.community_id,
1136 user_id: i.props.node.comment.creator_id,
1138 WebSocketService.Instance.transferCommunity(form);
1139 i.state.showConfirmTransferCommunity = false;
1140 i.setState(i.state);
1143 handleShowConfirmTransferSite(i: CommentNode) {
1144 i.state.showConfirmTransferSite = true;
1145 i.setState(i.state);
1148 handleCancelShowConfirmTransferSite(i: CommentNode) {
1149 i.state.showConfirmTransferSite = false;
1150 i.setState(i.state);
1153 handleTransferSite(i: CommentNode) {
1154 let form: TransferSiteForm = {
1155 user_id: i.props.node.comment.creator_id,
1157 WebSocketService.Instance.transferSite(form);
1158 i.state.showConfirmTransferSite = false;
1159 i.setState(i.state);
1162 get isCommentNew(): boolean {
1163 let now = moment.utc().subtract(10, 'minutes');
1164 let then = moment.utc(this.props.node.comment.published);
1165 return now.isBefore(then);
1168 handleCommentCollapse(i: CommentNode) {
1169 i.state.collapsed = !i.state.collapsed;
1170 i.setState(i.state);
1173 handleViewSource(i: CommentNode) {
1174 i.state.viewSource = !i.state.viewSource;
1175 i.setState(i.state);
1178 handleShowAdvanced(i: CommentNode) {
1179 i.state.showAdvanced = !i.state.showAdvanced;
1180 i.setState(i.state);
1185 if (this.state.my_vote == 1) {
1187 } else if (this.state.my_vote == -1) {
1188 return 'text-danger';
1190 return 'text-muted';
1194 get pointsTippy(): string {
1195 let points = i18n.t('number_of_points', {
1196 count: this.state.score,
1199 let upvotes = i18n.t('number_of_upvotes', {
1200 count: this.state.upvotes,
1203 let downvotes = i18n.t('number_of_downvotes', {
1204 count: this.state.downvotes,
1207 return `${points} • ${upvotes} • ${downvotes}`;