1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
4 CommentNode as CommentNodeI,
9 MarkUserMentionAsReadForm,
15 AddModToCommunityForm,
17 TransferCommunityForm,
20 } from 'lemmy-js-client';
21 import { CommentSortType, BanType } from '../interfaces';
22 import { WebSocketService, UserService } from '../services';
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 { UserListing } from './user-listing';
36 import { CommunityLink } from './community-link';
37 import { i18n } from '../i18next';
39 interface CommentNodeState {
42 showRemoveDialog: boolean;
44 showBanDialog: boolean;
49 showConfirmTransferSite: boolean;
50 showConfirmTransferCommunity: boolean;
51 showConfirmAppointAsMod: boolean;
52 showConfirmAppointAsAdmin: boolean;
55 showAdvanced: boolean;
65 interface CommentNodeProps {
72 showContext?: boolean;
73 moderators: CommunityUser[];
75 // TODO is this necessary, can't I get it from the node itself?
76 postCreatorId?: number;
77 showCommunity?: boolean;
78 sort?: CommentSortType;
80 enableDownvotes: boolean;
83 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
84 private emptyState: CommentNodeState = {
87 showRemoveDialog: false,
93 banType: BanType.Community,
97 showConfirmTransferSite: false,
98 showConfirmTransferCommunity: false,
99 showConfirmAppointAsMod: false,
100 showConfirmAppointAsAdmin: false,
101 my_vote: this.props.node.comment.my_vote,
102 score: this.props.node.comment.score,
103 upvotes: this.props.node.comment.upvotes,
104 downvotes: this.props.node.comment.downvotes,
105 borderColor: this.props.node.comment.depth
106 ? colorList[this.props.node.comment.depth % colorList.length]
112 constructor(props: any, context: any) {
113 super(props, context);
115 this.state = this.emptyState;
116 this.handleReplyCancel = this.handleReplyCancel.bind(this);
117 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
118 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
121 componentWillReceiveProps(nextProps: CommentNodeProps) {
122 this.state.my_vote = nextProps.node.comment.my_vote;
123 this.state.upvotes = nextProps.node.comment.upvotes;
124 this.state.downvotes = nextProps.node.comment.downvotes;
125 this.state.score = nextProps.node.comment.score;
126 this.state.readLoading = false;
127 this.state.saveLoading = false;
128 this.setState(this.state);
132 let node = this.props.node;
135 className={`comment ${
136 node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
140 id={`comment-${node.comment.id}`}
141 className={`details comment-node py-2 ${
142 !this.props.noBorder ? 'border-top border-light' : ''
143 } ${this.isCommentNew ? 'mark' : ''}`}
145 !this.props.noIndent &&
146 this.props.node.comment.parent_id &&
147 `border-left: 2px ${this.state.borderColor} solid !important`
152 !this.props.noIndent &&
153 this.props.node.comment.parent_id &&
157 <div class="d-flex flex-wrap align-items-center text-muted small">
161 name: node.comment.creator_name,
162 preferred_username: node.comment.creator_preferred_username,
163 avatar: node.comment.creator_avatar,
164 id: node.comment.creator_id,
165 local: node.comment.creator_local,
166 actor_id: node.comment.creator_actor_id,
167 published: node.comment.creator_published,
173 <div className="badge badge-light d-none d-sm-inline mr-2">
178 <div className="badge badge-light d-none d-sm-inline mr-2">
182 {this.isPostCreator && (
183 <div className="badge badge-light d-none d-sm-inline mr-2">
187 {(node.comment.banned_from_community || node.comment.banned) && (
188 <div className="badge badge-danger mr-2">
192 {this.props.showCommunity && (
194 <span class="mx-1">{i18n.t('to')}</span>
197 name: node.comment.community_name,
198 id: node.comment.community_id,
199 local: node.comment.community_local,
200 actor_id: node.comment.community_actor_id,
201 icon: node.comment.community_icon,
204 <span class="mx-2">•</span>
205 <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
206 {node.comment.post_name}
211 class="btn text-muted"
212 onClick={linkEvent(this, this.handleCommentCollapse)}
214 {this.state.collapsed ? (
215 <svg class="icon icon-inline">
216 <use xlinkHref="#icon-plus-square"></use>
219 <svg class="icon icon-inline">
220 <use xlinkHref="#icon-minus-square"></use>
224 {/* This is an expanding spacer for mobile */}
225 <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
227 className={`btn p-0 unselectable pointer ${this.scoreColor}`}
228 onClick={linkEvent(node, this.handleCommentUpvote)}
229 data-tippy-content={this.pointsTippy}
231 <svg class="icon icon-inline mr-1">
232 <use xlinkHref="#icon-zap"></use>
234 <span class="mr-1">{this.state.score}</span>
236 <span className="mr-1">•</span>
238 <MomentTime data={node.comment} />
241 {/* end of user row */}
242 {this.state.showEdit && (
246 onReplyCancel={this.handleReplyCancel}
247 disabled={this.props.locked}
251 {!this.state.showEdit && !this.state.collapsed && (
253 {this.state.viewSource ? (
254 <pre>{this.commentUnlessRemoved}</pre>
258 dangerouslySetInnerHTML={mdToHtml(
259 this.commentUnlessRemoved
263 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
264 {this.props.showContext && this.linkBtn}
265 {this.props.markable && (
267 class="btn btn-link btn-animate text-muted"
268 onClick={linkEvent(this, this.handleMarkRead)}
271 ? i18n.t('mark_as_unread')
272 : i18n.t('mark_as_read')
275 {this.state.readLoading ? (
279 class={`icon icon-inline ${
280 node.comment.read && 'text-success'
283 <use xlinkHref="#icon-check"></use>
288 {UserService.Instance.user && !this.props.viewOnly && (
291 className={`btn btn-link btn-animate ${
292 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
294 onClick={linkEvent(node, this.handleCommentUpvote)}
295 data-tippy-content={i18n.t('upvote')}
297 <svg class="icon icon-inline">
298 <use xlinkHref="#icon-arrow-up"></use>
300 {this.state.upvotes !== this.state.score && (
301 <span class="ml-1">{this.state.upvotes}</span>
304 {this.props.enableDownvotes && (
306 className={`btn btn-link btn-animate ${
307 this.state.my_vote == -1
311 onClick={linkEvent(node, this.handleCommentDownvote)}
312 data-tippy-content={i18n.t('downvote')}
314 <svg class="icon icon-inline">
315 <use xlinkHref="#icon-arrow-down"></use>
317 {this.state.upvotes !== this.state.score && (
318 <span class="ml-1">{this.state.downvotes}</span>
323 class="btn btn-link btn-animate text-muted"
324 onClick={linkEvent(this, this.handleReplyClick)}
325 data-tippy-content={i18n.t('reply')}
327 <svg class="icon icon-inline">
328 <use xlinkHref="#icon-reply1"></use>
331 {!this.state.showAdvanced ? (
333 className="btn btn-link btn-animate text-muted"
334 onClick={linkEvent(this, this.handleShowAdvanced)}
335 data-tippy-content={i18n.t('more')}
337 <svg class="icon icon-inline">
338 <use xlinkHref="#icon-more-vertical"></use>
343 {!this.myComment && (
344 <button class="btn btn-link btn-animate">
347 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
348 title={i18n.t('message').toLowerCase()}
351 <use xlinkHref="#icon-mail"></use>
356 {!this.props.showContext && this.linkBtn}
358 class="btn btn-link btn-animate text-muted"
361 this.handleSaveCommentClick
369 {this.state.saveLoading ? (
373 class={`icon icon-inline ${
374 node.comment.saved && 'text-warning'
377 <use xlinkHref="#icon-star"></use>
382 className="btn btn-link btn-animate text-muted"
383 onClick={linkEvent(this, this.handleViewSource)}
384 data-tippy-content={i18n.t('view_source')}
387 class={`icon icon-inline ${
388 this.state.viewSource && 'text-success'
391 <use xlinkHref="#icon-file-text"></use>
397 class="btn btn-link btn-animate text-muted"
398 onClick={linkEvent(this, this.handleEditClick)}
399 data-tippy-content={i18n.t('edit')}
401 <svg class="icon icon-inline">
402 <use xlinkHref="#icon-edit"></use>
406 class="btn btn-link btn-animate text-muted"
409 this.handleDeleteClick
412 !node.comment.deleted
418 class={`icon icon-inline ${
419 node.comment.deleted && 'text-danger'
422 <use xlinkHref="#icon-trash"></use>
427 {/* Admins and mods can remove comments */}
428 {(this.canMod || this.canAdmin) && (
430 {!node.comment.removed ? (
432 class="btn btn-link btn-animate text-muted"
435 this.handleModRemoveShow
442 class="btn btn-link btn-animate text-muted"
445 this.handleModRemoveSubmit
453 {/* Mods can ban from community, and appoint as mods to community */}
457 (!node.comment.banned_from_community ? (
459 class="btn btn-link btn-animate text-muted"
462 this.handleModBanFromCommunityShow
469 class="btn btn-link btn-animate text-muted"
472 this.handleModBanFromCommunitySubmit
478 {!node.comment.banned_from_community &&
479 node.comment.creator_local &&
480 (!this.state.showConfirmAppointAsMod ? (
482 class="btn btn-link btn-animate text-muted"
485 this.handleShowConfirmAppointAsMod
489 ? i18n.t('remove_as_mod')
490 : i18n.t('appoint_as_mod')}
494 <button class="btn btn-link btn-animate text-muted">
495 {i18n.t('are_you_sure')}
498 class="btn btn-link btn-animate text-muted"
501 this.handleAddModToCommunity
507 class="btn btn-link btn-animate text-muted"
510 this.handleCancelConfirmAppointAsMod
519 {/* Community creators and admins can transfer community to another mod */}
520 {(this.amCommunityCreator || this.canAdmin) &&
522 node.comment.creator_local &&
523 (!this.state.showConfirmTransferCommunity ? (
525 class="btn btn-link btn-animate text-muted"
528 this.handleShowConfirmTransferCommunity
531 {i18n.t('transfer_community')}
535 <button class="btn btn-link btn-animate text-muted">
536 {i18n.t('are_you_sure')}
539 class="btn btn-link btn-animate text-muted"
542 this.handleTransferCommunity
548 class="btn btn-link btn-animate text-muted"
552 .handleCancelShowConfirmTransferCommunity
559 {/* Admins can ban from all, and appoint other admins */}
563 (!node.comment.banned ? (
565 class="btn btn-link btn-animate text-muted"
568 this.handleModBanShow
571 {i18n.t('ban_from_site')}
575 class="btn btn-link btn-animate text-muted"
578 this.handleModBanSubmit
581 {i18n.t('unban_from_site')}
584 {!node.comment.banned &&
585 node.comment.creator_local &&
586 (!this.state.showConfirmAppointAsAdmin ? (
588 class="btn btn-link btn-animate text-muted"
591 this.handleShowConfirmAppointAsAdmin
595 ? i18n.t('remove_as_admin')
596 : i18n.t('appoint_as_admin')}
600 <button class="btn btn-link btn-animate text-muted">
601 {i18n.t('are_you_sure')}
604 class="btn btn-link btn-animate text-muted"
613 class="btn btn-link btn-animate text-muted"
616 this.handleCancelConfirmAppointAsAdmin
625 {/* Site Creator can transfer to another admin */}
626 {this.amSiteCreator &&
628 node.comment.creator_local &&
629 (!this.state.showConfirmTransferSite ? (
631 class="btn btn-link btn-animate text-muted"
634 this.handleShowConfirmTransferSite
637 {i18n.t('transfer_site')}
641 <button class="btn btn-link btn-animate text-muted">
642 {i18n.t('are_you_sure')}
645 class="btn btn-link btn-animate text-muted"
648 this.handleTransferSite
654 class="btn btn-link btn-animate text-muted"
657 this.handleCancelShowConfirmTransferSite
669 {/* end of button group */}
674 {/* end of details */}
675 {this.state.showRemoveDialog && (
678 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
682 class="form-control mr-2"
683 placeholder={i18n.t('reason')}
684 value={this.state.removeReason}
685 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
687 <button type="submit" class="btn btn-secondary">
688 {i18n.t('remove_comment')}
692 {this.state.showBanDialog && (
693 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
694 <div class="form-group row">
695 <label class="col-form-label">{i18n.t('reason')}</label>
698 class="form-control mr-2"
699 placeholder={i18n.t('reason')}
700 value={this.state.banReason}
701 onInput={linkEvent(this, this.handleModBanReasonChange)}
703 <div class="form-group">
704 <div class="form-check">
706 class="form-check-input"
707 id="mod-ban-remove-data"
709 checked={this.state.removeData}
710 onChange={linkEvent(this, this.handleModRemoveDataChange)}
712 <label class="form-check-label" htmlFor="mod-ban-remove-data">
713 {i18n.t('remove_posts_comments')}
718 {/* TODO hold off on expires until later */}
719 {/* <div class="form-group row"> */}
720 {/* <label class="col-form-label">Expires</label> */}
721 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
723 <div class="form-group row">
724 <button type="submit" class="btn btn-secondary">
725 {i18n.t('ban')} {node.comment.creator_name}
730 {this.state.showReply && (
733 onReplyCancel={this.handleReplyCancel}
734 disabled={this.props.locked}
738 {node.children && !this.state.collapsed && (
740 nodes={node.children}
741 locked={this.props.locked}
742 moderators={this.props.moderators}
743 admins={this.props.admins}
744 postCreatorId={this.props.postCreatorId}
745 sort={this.props.sort}
746 sortType={this.props.sortType}
747 enableDownvotes={this.props.enableDownvotes}
750 {/* A collapsed clearfix */}
751 {this.state.collapsed && <div class="row col-12"></div>}
757 let node = this.props.node;
760 class="btn btn-link btn-animate text-muted"
761 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
762 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
764 <svg class="icon icon-inline">
765 <use xlinkHref="#icon-link"></use>
773 <svg class="icon icon-spinner spin">
774 <use xlinkHref="#icon-spinner"></use>
779 get myComment(): boolean {
781 UserService.Instance.user &&
782 this.props.node.comment.creator_id == UserService.Instance.user.id
786 get isMod(): boolean {
788 this.props.moderators &&
790 this.props.moderators.map(m => m.user_id),
791 this.props.node.comment.creator_id
796 get isAdmin(): boolean {
800 this.props.admins.map(a => a.id),
801 this.props.node.comment.creator_id
806 get isPostCreator(): boolean {
807 return this.props.node.comment.creator_id == this.props.postCreatorId;
810 get canMod(): boolean {
811 if (this.props.admins && this.props.moderators) {
812 let adminsThenMods = this.props.admins
814 .concat(this.props.moderators.map(m => m.user_id));
817 UserService.Instance.user,
819 this.props.node.comment.creator_id
826 get canAdmin(): boolean {
830 UserService.Instance.user,
831 this.props.admins.map(a => a.id),
832 this.props.node.comment.creator_id
837 get amCommunityCreator(): boolean {
839 this.props.moderators &&
840 UserService.Instance.user &&
841 this.props.node.comment.creator_id != UserService.Instance.user.id &&
842 UserService.Instance.user.id == this.props.moderators[0].user_id
846 get amSiteCreator(): boolean {
849 UserService.Instance.user &&
850 this.props.node.comment.creator_id != UserService.Instance.user.id &&
851 UserService.Instance.user.id == this.props.admins[0].id
855 get commentUnlessRemoved(): string {
856 let node = this.props.node;
857 return node.comment.removed
858 ? `*${i18n.t('removed')}*`
859 : node.comment.deleted
860 ? `*${i18n.t('deleted')}*`
861 : node.comment.content;
864 handleReplyClick(i: CommentNode) {
865 i.state.showReply = true;
869 handleEditClick(i: CommentNode) {
870 i.state.showEdit = true;
874 handleDeleteClick(i: CommentNode) {
875 let deleteForm: DeleteCommentForm = {
876 edit_id: i.props.node.comment.id,
877 deleted: !i.props.node.comment.deleted,
880 WebSocketService.Instance.deleteComment(deleteForm);
883 handleSaveCommentClick(i: CommentNode) {
885 i.props.node.comment.saved == undefined
887 : !i.props.node.comment.saved;
888 let form: SaveCommentForm = {
889 comment_id: i.props.node.comment.id,
893 WebSocketService.Instance.saveComment(form);
895 i.state.saveLoading = true;
896 i.setState(this.state);
899 handleReplyCancel() {
900 this.state.showReply = false;
901 this.state.showEdit = false;
902 this.setState(this.state);
905 handleCommentUpvote(i: CommentNodeI) {
906 let new_vote = this.state.my_vote == 1 ? 0 : 1;
908 if (this.state.my_vote == 1) {
910 this.state.upvotes--;
911 } else if (this.state.my_vote == -1) {
912 this.state.downvotes--;
913 this.state.upvotes++;
914 this.state.score += 2;
916 this.state.upvotes++;
920 this.state.my_vote = new_vote;
922 let form: CommentLikeForm = {
923 comment_id: i.comment.id,
924 score: this.state.my_vote,
927 WebSocketService.Instance.likeComment(form);
928 this.setState(this.state);
932 handleCommentDownvote(i: CommentNodeI) {
933 let new_vote = this.state.my_vote == -1 ? 0 : -1;
935 if (this.state.my_vote == 1) {
936 this.state.score -= 2;
937 this.state.upvotes--;
938 this.state.downvotes++;
939 } else if (this.state.my_vote == -1) {
940 this.state.downvotes--;
943 this.state.downvotes++;
947 this.state.my_vote = new_vote;
949 let form: CommentLikeForm = {
950 comment_id: i.comment.id,
951 score: this.state.my_vote,
954 WebSocketService.Instance.likeComment(form);
955 this.setState(this.state);
959 handleModRemoveShow(i: CommentNode) {
960 i.state.showRemoveDialog = true;
964 handleModRemoveReasonChange(i: CommentNode, event: any) {
965 i.state.removeReason = event.target.value;
969 handleModRemoveDataChange(i: CommentNode, event: any) {
970 i.state.removeData = event.target.checked;
974 handleModRemoveSubmit(i: CommentNode) {
975 event.preventDefault();
976 let form: RemoveCommentForm = {
977 edit_id: i.props.node.comment.id,
978 removed: !i.props.node.comment.removed,
979 reason: i.state.removeReason,
982 WebSocketService.Instance.removeComment(form);
984 i.state.showRemoveDialog = false;
988 handleMarkRead(i: CommentNode) {
989 // if it has a user_mention_id field, then its a mention
990 if (i.props.node.comment.user_mention_id) {
991 let form: MarkUserMentionAsReadForm = {
992 user_mention_id: i.props.node.comment.user_mention_id,
993 read: !i.props.node.comment.read,
995 WebSocketService.Instance.markUserMentionAsRead(form);
997 let form: MarkCommentAsReadForm = {
998 edit_id: i.props.node.comment.id,
999 read: !i.props.node.comment.read,
1002 WebSocketService.Instance.markCommentAsRead(form);
1005 i.state.readLoading = true;
1006 i.setState(this.state);
1009 handleModBanFromCommunityShow(i: CommentNode) {
1010 i.state.showBanDialog = !i.state.showBanDialog;
1011 i.state.banType = BanType.Community;
1012 i.setState(i.state);
1015 handleModBanShow(i: CommentNode) {
1016 i.state.showBanDialog = !i.state.showBanDialog;
1017 i.state.banType = BanType.Site;
1018 i.setState(i.state);
1021 handleModBanReasonChange(i: CommentNode, event: any) {
1022 i.state.banReason = event.target.value;
1023 i.setState(i.state);
1026 handleModBanExpiresChange(i: CommentNode, event: any) {
1027 i.state.banExpires = event.target.value;
1028 i.setState(i.state);
1031 handleModBanFromCommunitySubmit(i: CommentNode) {
1032 i.state.banType = BanType.Community;
1033 i.setState(i.state);
1034 i.handleModBanBothSubmit(i);
1037 handleModBanSubmit(i: CommentNode) {
1038 i.state.banType = BanType.Site;
1039 i.setState(i.state);
1040 i.handleModBanBothSubmit(i);
1043 handleModBanBothSubmit(i: CommentNode) {
1044 event.preventDefault();
1046 if (i.state.banType == BanType.Community) {
1047 // If its an unban, restore all their data
1048 let ban = !i.props.node.comment.banned_from_community;
1050 i.state.removeData = false;
1052 let form: BanFromCommunityForm = {
1053 user_id: i.props.node.comment.creator_id,
1054 community_id: i.props.node.comment.community_id,
1056 remove_data: i.state.removeData,
1057 reason: i.state.banReason,
1058 expires: getUnixTime(i.state.banExpires),
1060 WebSocketService.Instance.banFromCommunity(form);
1062 // If its an unban, restore all their data
1063 let ban = !i.props.node.comment.banned;
1065 i.state.removeData = false;
1067 let form: BanUserForm = {
1068 user_id: i.props.node.comment.creator_id,
1070 remove_data: i.state.removeData,
1071 reason: i.state.banReason,
1072 expires: getUnixTime(i.state.banExpires),
1074 WebSocketService.Instance.banUser(form);
1077 i.state.showBanDialog = false;
1078 i.setState(i.state);
1081 handleShowConfirmAppointAsMod(i: CommentNode) {
1082 i.state.showConfirmAppointAsMod = true;
1083 i.setState(i.state);
1086 handleCancelConfirmAppointAsMod(i: CommentNode) {
1087 i.state.showConfirmAppointAsMod = false;
1088 i.setState(i.state);
1091 handleAddModToCommunity(i: CommentNode) {
1092 let form: AddModToCommunityForm = {
1093 user_id: i.props.node.comment.creator_id,
1094 community_id: i.props.node.comment.community_id,
1097 WebSocketService.Instance.addModToCommunity(form);
1098 i.state.showConfirmAppointAsMod = false;
1099 i.setState(i.state);
1102 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1103 i.state.showConfirmAppointAsAdmin = true;
1104 i.setState(i.state);
1107 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1108 i.state.showConfirmAppointAsAdmin = false;
1109 i.setState(i.state);
1112 handleAddAdmin(i: CommentNode) {
1113 let form: AddAdminForm = {
1114 user_id: i.props.node.comment.creator_id,
1117 WebSocketService.Instance.addAdmin(form);
1118 i.state.showConfirmAppointAsAdmin = false;
1119 i.setState(i.state);
1122 handleShowConfirmTransferCommunity(i: CommentNode) {
1123 i.state.showConfirmTransferCommunity = true;
1124 i.setState(i.state);
1127 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1128 i.state.showConfirmTransferCommunity = false;
1129 i.setState(i.state);
1132 handleTransferCommunity(i: CommentNode) {
1133 let form: TransferCommunityForm = {
1134 community_id: i.props.node.comment.community_id,
1135 user_id: i.props.node.comment.creator_id,
1137 WebSocketService.Instance.transferCommunity(form);
1138 i.state.showConfirmTransferCommunity = false;
1139 i.setState(i.state);
1142 handleShowConfirmTransferSite(i: CommentNode) {
1143 i.state.showConfirmTransferSite = true;
1144 i.setState(i.state);
1147 handleCancelShowConfirmTransferSite(i: CommentNode) {
1148 i.state.showConfirmTransferSite = false;
1149 i.setState(i.state);
1152 handleTransferSite(i: CommentNode) {
1153 let form: TransferSiteForm = {
1154 user_id: i.props.node.comment.creator_id,
1156 WebSocketService.Instance.transferSite(form);
1157 i.state.showConfirmTransferSite = false;
1158 i.setState(i.state);
1161 get isCommentNew(): boolean {
1162 let now = moment.utc().subtract(10, 'minutes');
1163 let then = moment.utc(this.props.node.comment.published);
1164 return now.isBefore(then);
1167 handleCommentCollapse(i: CommentNode) {
1168 i.state.collapsed = !i.state.collapsed;
1169 i.setState(i.state);
1172 handleViewSource(i: CommentNode) {
1173 i.state.viewSource = !i.state.viewSource;
1174 i.setState(i.state);
1177 handleShowAdvanced(i: CommentNode) {
1178 i.state.showAdvanced = !i.state.showAdvanced;
1179 i.setState(i.state);
1184 if (this.state.my_vote == 1) {
1186 } else if (this.state.my_vote == -1) {
1187 return 'text-danger';
1189 return 'text-muted';
1193 get pointsTippy(): string {
1194 let points = i18n.t('number_of_points', {
1195 count: this.state.score,
1198 let upvotes = i18n.t('number_of_upvotes', {
1199 count: this.state.upvotes,
1202 let downvotes = i18n.t('number_of_downvotes', {
1203 count: this.state.downvotes,
1206 return `${points} • ${upvotes} • ${downvotes}`;