1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
12 CommunityModeratorView,
21 } from 'lemmy-js-client';
24 CommentNode as CommentNodeI,
26 } from '../interfaces';
27 import { WebSocketService, UserService } from '../services';
36 import moment from 'moment';
37 import { MomentTime } from './moment-time';
38 import { CommentForm } from './comment-form';
39 import { CommentNodes } from './comment-nodes';
40 import { UserListing } from './user-listing';
41 import { CommunityLink } from './community-link';
42 import { i18n } from '../i18next';
44 interface CommentNodeState {
47 showRemoveDialog: boolean;
49 showBanDialog: boolean;
54 showConfirmTransferSite: boolean;
55 showConfirmTransferCommunity: boolean;
56 showConfirmAppointAsMod: boolean;
57 showConfirmAppointAsAdmin: boolean;
60 showAdvanced: boolean;
70 interface CommentNodeProps {
77 showContext?: boolean;
78 moderators: CommunityModeratorView[];
79 admins: UserViewSafe[];
80 // TODO is this necessary, can't I get it from the node itself?
81 postCreatorId?: number;
82 showCommunity?: boolean;
83 sort?: CommentSortType;
85 enableDownvotes: boolean;
88 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
89 private emptyState: CommentNodeState = {
92 showRemoveDialog: false,
98 banType: BanType.Community,
102 showConfirmTransferSite: false,
103 showConfirmTransferCommunity: false,
104 showConfirmAppointAsMod: false,
105 showConfirmAppointAsAdmin: false,
106 my_vote: this.props.node.comment_view.my_vote,
107 score: this.props.node.comment_view.counts.score,
108 upvotes: this.props.node.comment_view.counts.upvotes,
109 downvotes: this.props.node.comment_view.counts.downvotes,
110 borderColor: this.props.node.depth
111 ? colorList[this.props.node.depth % colorList.length]
117 constructor(props: any, context: any) {
118 super(props, context);
120 this.state = this.emptyState;
121 this.handleReplyCancel = this.handleReplyCancel.bind(this);
122 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
123 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
126 // TODO see if there's a better way to do this, and all willReceiveProps
127 componentWillReceiveProps(nextProps: CommentNodeProps) {
128 let cv = nextProps.node.comment_view;
129 this.state.my_vote = cv.my_vote;
130 this.state.upvotes = cv.counts.upvotes;
131 this.state.downvotes = cv.counts.downvotes;
132 this.state.score = cv.counts.score;
133 this.state.readLoading = false;
134 this.state.saveLoading = false;
135 this.setState(this.state);
139 let node = this.props.node;
140 let cv = this.props.node.comment_view;
143 className={`comment ${
144 cv.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
148 id={`comment-${cv.comment.id}`}
149 className={`details comment-node py-2 ${
150 !this.props.noBorder ? 'border-top border-light' : ''
151 } ${this.isCommentNew ? 'mark' : ''}`}
153 !this.props.noIndent &&
154 cv.comment.parent_id &&
155 `border-left: 2px ${this.state.borderColor} solid !important`
159 class={`${!this.props.noIndent && cv.comment.parent_id && 'ml-2'}`}
161 <div class="d-flex flex-wrap align-items-center text-muted small">
163 <UserListing user={cv.creator} />
167 <div className="badge badge-light d-none d-sm-inline mr-2">
172 <div className="badge badge-light d-none d-sm-inline mr-2">
176 {this.isPostCreator && (
177 <div className="badge badge-light d-none d-sm-inline mr-2">
181 {(cv.creator_banned_from_community || cv.creator.banned) && (
182 <div className="badge badge-danger mr-2">
186 {this.props.showCommunity && (
188 <span class="mx-1">{i18n.t('to')}</span>
189 <CommunityLink community={cv.community} />
190 <span class="mx-2">•</span>
191 <Link className="mr-2" to={`/post/${cv.post.id}`}>
197 class="btn btn-sm text-muted"
198 onClick={linkEvent(this, this.handleCommentCollapse)}
200 {this.state.collapsed ? '+' : '—'}
202 {/* This is an expanding spacer for mobile */}
203 <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
205 className={`unselectable pointer ${this.scoreColor}`}
206 onClick={linkEvent(node, this.handleCommentUpvote)}
207 data-tippy-content={this.pointsTippy}
209 <span class="mr-1 font-weight-bold">{this.state.score}</span>
211 <span className="mr-1">•</span>
213 <MomentTime data={cv.comment} />
216 {/* end of user row */}
217 {this.state.showEdit && (
221 onReplyCancel={this.handleReplyCancel}
222 disabled={this.props.locked}
226 {!this.state.showEdit && !this.state.collapsed && (
228 {this.state.viewSource ? (
229 <pre>{this.commentUnlessRemoved}</pre>
233 dangerouslySetInnerHTML={mdToHtml(
234 this.commentUnlessRemoved
238 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
239 {this.props.showContext && this.linkBtn}
240 {this.props.markable && (
242 class="btn btn-link btn-animate text-muted"
243 onClick={linkEvent(this, this.handleMarkRead)}
246 ? i18n.t('mark_as_unread')
247 : i18n.t('mark_as_read')
250 {this.state.readLoading ? (
254 class={`icon icon-inline ${
255 cv.comment.read && 'text-success'
258 <use xlinkHref="#icon-check"></use>
263 {UserService.Instance.user && !this.props.viewOnly && (
266 className={`btn btn-link btn-animate ${
267 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
269 onClick={linkEvent(node, this.handleCommentUpvote)}
270 data-tippy-content={i18n.t('upvote')}
272 <svg class="icon icon-inline">
273 <use xlinkHref="#icon-arrow-up1"></use>
275 {this.state.upvotes !== this.state.score && (
276 <span class="ml-1">{this.state.upvotes}</span>
279 {this.props.enableDownvotes && (
281 className={`btn btn-link btn-animate ${
282 this.state.my_vote == -1
286 onClick={linkEvent(node, this.handleCommentDownvote)}
287 data-tippy-content={i18n.t('downvote')}
289 <svg class="icon icon-inline">
290 <use xlinkHref="#icon-arrow-down1"></use>
292 {this.state.upvotes !== this.state.score && (
293 <span class="ml-1">{this.state.downvotes}</span>
298 class="btn btn-link btn-animate text-muted"
299 onClick={linkEvent(this, this.handleReplyClick)}
300 data-tippy-content={i18n.t('reply')}
302 <svg class="icon icon-inline">
303 <use xlinkHref="#icon-reply1"></use>
306 {!this.state.showAdvanced ? (
308 className="btn btn-link btn-animate text-muted"
309 onClick={linkEvent(this, this.handleShowAdvanced)}
310 data-tippy-content={i18n.t('more')}
312 <svg class="icon icon-inline">
313 <use xlinkHref="#icon-more-vertical"></use>
318 {!this.myComment && (
319 <button class="btn btn-link btn-animate">
321 className="text-muted"
322 to={`/create_private_message/recipient/${cv.creator.id}`}
323 title={i18n.t('message').toLowerCase()}
326 <use xlinkHref="#icon-mail"></use>
331 {!this.props.showContext && this.linkBtn}
333 class="btn btn-link btn-animate text-muted"
336 this.handleSaveCommentClick
339 cv.saved ? i18n.t('unsave') : i18n.t('save')
342 {this.state.saveLoading ? (
346 class={`icon icon-inline ${
347 cv.saved && 'text-warning'
350 <use xlinkHref="#icon-star"></use>
355 className="btn btn-link btn-animate text-muted"
356 onClick={linkEvent(this, this.handleViewSource)}
357 data-tippy-content={i18n.t('view_source')}
360 class={`icon icon-inline ${
361 this.state.viewSource && 'text-success'
364 <use xlinkHref="#icon-file-text"></use>
370 class="btn btn-link btn-animate text-muted"
371 onClick={linkEvent(this, this.handleEditClick)}
372 data-tippy-content={i18n.t('edit')}
374 <svg class="icon icon-inline">
375 <use xlinkHref="#icon-edit"></use>
379 class="btn btn-link btn-animate text-muted"
382 this.handleDeleteClick
391 class={`icon icon-inline ${
392 cv.comment.deleted && 'text-danger'
395 <use xlinkHref="#icon-trash"></use>
400 {/* Admins and mods can remove comments */}
401 {(this.canMod || this.canAdmin) && (
403 {!cv.comment.removed ? (
405 class="btn btn-link btn-animate text-muted"
408 this.handleModRemoveShow
415 class="btn btn-link btn-animate text-muted"
418 this.handleModRemoveSubmit
426 {/* Mods can ban from community, and appoint as mods to community */}
430 (!cv.creator_banned_from_community ? (
432 class="btn btn-link btn-animate text-muted"
435 this.handleModBanFromCommunityShow
442 class="btn btn-link btn-animate text-muted"
445 this.handleModBanFromCommunitySubmit
451 {!cv.creator_banned_from_community &&
453 (!this.state.showConfirmAppointAsMod ? (
455 class="btn btn-link btn-animate text-muted"
458 this.handleShowConfirmAppointAsMod
462 ? i18n.t('remove_as_mod')
463 : i18n.t('appoint_as_mod')}
467 <button class="btn btn-link btn-animate text-muted">
468 {i18n.t('are_you_sure')}
471 class="btn btn-link btn-animate text-muted"
474 this.handleAddModToCommunity
480 class="btn btn-link btn-animate text-muted"
483 this.handleCancelConfirmAppointAsMod
492 {/* Community creators and admins can transfer community to another mod */}
493 {(this.amCommunityCreator || this.canAdmin) &&
496 (!this.state.showConfirmTransferCommunity ? (
498 class="btn btn-link btn-animate text-muted"
501 this.handleShowConfirmTransferCommunity
504 {i18n.t('transfer_community')}
508 <button class="btn btn-link btn-animate text-muted">
509 {i18n.t('are_you_sure')}
512 class="btn btn-link btn-animate text-muted"
515 this.handleTransferCommunity
521 class="btn btn-link btn-animate text-muted"
525 .handleCancelShowConfirmTransferCommunity
532 {/* Admins can ban from all, and appoint other admins */}
536 (!cv.creator.banned ? (
538 class="btn btn-link btn-animate text-muted"
541 this.handleModBanShow
544 {i18n.t('ban_from_site')}
548 class="btn btn-link btn-animate text-muted"
551 this.handleModBanSubmit
554 {i18n.t('unban_from_site')}
557 {!cv.creator.banned &&
559 (!this.state.showConfirmAppointAsAdmin ? (
561 class="btn btn-link btn-animate text-muted"
564 this.handleShowConfirmAppointAsAdmin
568 ? i18n.t('remove_as_admin')
569 : i18n.t('appoint_as_admin')}
573 <button class="btn btn-link btn-animate text-muted">
574 {i18n.t('are_you_sure')}
577 class="btn btn-link btn-animate text-muted"
586 class="btn btn-link btn-animate text-muted"
589 this.handleCancelConfirmAppointAsAdmin
598 {/* Site Creator can transfer to another admin */}
599 {this.amSiteCreator &&
602 (!this.state.showConfirmTransferSite ? (
604 class="btn btn-link btn-animate text-muted"
607 this.handleShowConfirmTransferSite
610 {i18n.t('transfer_site')}
614 <button class="btn btn-link btn-animate text-muted">
615 {i18n.t('are_you_sure')}
618 class="btn btn-link btn-animate text-muted"
621 this.handleTransferSite
627 class="btn btn-link btn-animate text-muted"
630 this.handleCancelShowConfirmTransferSite
642 {/* end of button group */}
647 {/* end of details */}
648 {this.state.showRemoveDialog && (
651 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
655 class="form-control mr-2"
656 placeholder={i18n.t('reason')}
657 value={this.state.removeReason}
658 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
660 <button type="submit" class="btn btn-secondary">
661 {i18n.t('remove_comment')}
665 {this.state.showBanDialog && (
666 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
667 <div class="form-group row">
668 <label class="col-form-label">{i18n.t('reason')}</label>
671 class="form-control mr-2"
672 placeholder={i18n.t('reason')}
673 value={this.state.banReason}
674 onInput={linkEvent(this, this.handleModBanReasonChange)}
676 <div class="form-group">
677 <div class="form-check">
679 class="form-check-input"
680 id="mod-ban-remove-data"
682 checked={this.state.removeData}
683 onChange={linkEvent(this, this.handleModRemoveDataChange)}
685 <label class="form-check-label" htmlFor="mod-ban-remove-data">
686 {i18n.t('remove_posts_comments')}
691 {/* TODO hold off on expires until later */}
692 {/* <div class="form-group row"> */}
693 {/* <label class="col-form-label">Expires</label> */}
694 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
696 <div class="form-group row">
697 <button type="submit" class="btn btn-secondary">
698 {i18n.t('ban')} {cv.creator.name}
703 {this.state.showReply && (
706 onReplyCancel={this.handleReplyCancel}
707 disabled={this.props.locked}
711 {node.children && !this.state.collapsed && (
713 nodes={node.children}
714 locked={this.props.locked}
715 moderators={this.props.moderators}
716 admins={this.props.admins}
717 postCreatorId={this.props.postCreatorId}
718 sort={this.props.sort}
719 sortType={this.props.sortType}
720 enableDownvotes={this.props.enableDownvotes}
723 {/* A collapsed clearfix */}
724 {this.state.collapsed && <div class="row col-12"></div>}
730 let cv = this.props.node.comment_view;
733 className="btn btn-link btn-animate text-muted"
734 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
735 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
737 <svg class="icon icon-inline">
738 <use xlinkHref="#icon-link"></use>
746 <svg class="icon icon-spinner spin">
747 <use xlinkHref="#icon-spinner"></use>
752 get myComment(): boolean {
754 UserService.Instance.user &&
755 this.props.node.comment_view.creator.id == UserService.Instance.user.id
759 get isMod(): boolean {
761 this.props.moderators &&
763 this.props.moderators.map(m => m.moderator.id),
764 this.props.node.comment_view.creator.id
769 get isAdmin(): boolean {
773 this.props.admins.map(a => a.user.id),
774 this.props.node.comment_view.creator.id
779 get isPostCreator(): boolean {
780 return this.props.node.comment_view.creator.id == this.props.postCreatorId;
783 get canMod(): boolean {
784 if (this.props.admins && this.props.moderators) {
785 let adminsThenMods = this.props.admins
787 .concat(this.props.moderators.map(m => m.moderator.id));
790 UserService.Instance.user,
792 this.props.node.comment_view.creator.id
799 get canAdmin(): boolean {
803 UserService.Instance.user,
804 this.props.admins.map(a => a.user.id),
805 this.props.node.comment_view.creator.id
810 get amCommunityCreator(): boolean {
812 this.props.moderators &&
813 UserService.Instance.user &&
814 this.props.node.comment_view.creator.id != UserService.Instance.user.id &&
815 UserService.Instance.user.id == this.props.moderators[0].moderator.id
819 get amSiteCreator(): boolean {
822 UserService.Instance.user &&
823 this.props.node.comment_view.creator.id != UserService.Instance.user.id &&
824 UserService.Instance.user.id == this.props.admins[0].user.id
828 get commentUnlessRemoved(): string {
829 let comment = this.props.node.comment_view.comment;
830 return comment.removed
831 ? `*${i18n.t('removed')}*`
833 ? `*${i18n.t('deleted')}*`
837 handleReplyClick(i: CommentNode) {
838 i.state.showReply = true;
842 handleEditClick(i: CommentNode) {
843 i.state.showEdit = true;
847 handleDeleteClick(i: CommentNode) {
848 let comment = i.props.node.comment_view.comment;
849 let deleteForm: DeleteComment = {
851 deleted: !comment.deleted,
852 auth: UserService.Instance.authField(),
854 WebSocketService.Instance.client.deleteComment(deleteForm);
857 handleSaveCommentClick(i: CommentNode) {
858 let cv = i.props.node.comment_view;
859 let save = cv.saved == undefined ? true : !cv.saved;
860 let form: SaveComment = {
861 comment_id: cv.comment.id,
863 auth: UserService.Instance.authField(),
866 WebSocketService.Instance.client.saveComment(form);
868 i.state.saveLoading = true;
869 i.setState(this.state);
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: CreateCommentLike = {
896 comment_id: i.comment_view.comment.id,
897 score: this.state.my_vote,
898 auth: UserService.Instance.authField(),
901 WebSocketService.Instance.client.likeComment(form);
902 this.setState(this.state);
906 handleCommentDownvote(i: CommentNodeI) {
907 let new_vote = this.state.my_vote == -1 ? 0 : -1;
909 if (this.state.my_vote == 1) {
910 this.state.score -= 2;
911 this.state.upvotes--;
912 this.state.downvotes++;
913 } else if (this.state.my_vote == -1) {
914 this.state.downvotes--;
917 this.state.downvotes++;
921 this.state.my_vote = new_vote;
923 let form: CreateCommentLike = {
924 comment_id: i.comment_view.comment.id,
925 score: this.state.my_vote,
926 auth: UserService.Instance.authField(),
929 WebSocketService.Instance.client.likeComment(form);
930 this.setState(this.state);
934 handleModRemoveShow(i: CommentNode) {
935 i.state.showRemoveDialog = true;
939 handleModRemoveReasonChange(i: CommentNode, event: any) {
940 i.state.removeReason = event.target.value;
944 handleModRemoveDataChange(i: CommentNode, event: any) {
945 i.state.removeData = event.target.checked;
949 handleModRemoveSubmit(i: CommentNode) {
950 let comment = i.props.node.comment_view.comment;
951 let form: RemoveComment = {
953 removed: !comment.removed,
954 reason: i.state.removeReason,
955 auth: UserService.Instance.authField(),
957 WebSocketService.Instance.client.removeComment(form);
959 i.state.showRemoveDialog = false;
964 item: CommentView | UserMentionView
965 ): item is UserMentionView {
966 return (item as UserMentionView).user_mention.id !== undefined;
969 handleMarkRead(i: CommentNode) {
970 if (i.isUserMentionType(i.props.node.comment_view)) {
971 let form: MarkUserMentionAsRead = {
972 user_mention_id: i.props.node.comment_view.user_mention.id,
973 read: !i.props.node.comment_view.user_mention.read,
974 auth: UserService.Instance.authField(),
976 WebSocketService.Instance.client.markUserMentionAsRead(form);
978 let form: MarkCommentAsRead = {
979 comment_id: i.props.node.comment_view.comment.id,
980 read: !i.props.node.comment_view.comment.read,
981 auth: UserService.Instance.authField(),
983 WebSocketService.Instance.client.markCommentAsRead(form);
986 i.state.readLoading = true;
987 i.setState(this.state);
990 handleModBanFromCommunityShow(i: CommentNode) {
991 i.state.showBanDialog = !i.state.showBanDialog;
992 i.state.banType = BanType.Community;
996 handleModBanShow(i: CommentNode) {
997 i.state.showBanDialog = !i.state.showBanDialog;
998 i.state.banType = BanType.Site;
1002 handleModBanReasonChange(i: CommentNode, event: any) {
1003 i.state.banReason = event.target.value;
1004 i.setState(i.state);
1007 handleModBanExpiresChange(i: CommentNode, event: any) {
1008 i.state.banExpires = event.target.value;
1009 i.setState(i.state);
1012 handleModBanFromCommunitySubmit(i: CommentNode) {
1013 i.state.banType = BanType.Community;
1014 i.setState(i.state);
1015 i.handleModBanBothSubmit(i);
1018 handleModBanSubmit(i: CommentNode) {
1019 i.state.banType = BanType.Site;
1020 i.setState(i.state);
1021 i.handleModBanBothSubmit(i);
1024 handleModBanBothSubmit(i: CommentNode) {
1025 let cv = i.props.node.comment_view;
1027 if (i.state.banType == BanType.Community) {
1028 // If its an unban, restore all their data
1029 let ban = !cv.creator_banned_from_community;
1031 i.state.removeData = false;
1033 let form: BanFromCommunity = {
1034 user_id: cv.creator.id,
1035 community_id: cv.community.id,
1037 remove_data: i.state.removeData,
1038 reason: i.state.banReason,
1039 expires: getUnixTime(i.state.banExpires),
1040 auth: UserService.Instance.authField(),
1042 WebSocketService.Instance.client.banFromCommunity(form);
1044 // If its an unban, restore all their data
1045 let ban = !cv.creator.banned;
1047 i.state.removeData = false;
1049 let form: BanUser = {
1050 user_id: cv.creator.id,
1052 remove_data: i.state.removeData,
1053 reason: i.state.banReason,
1054 expires: getUnixTime(i.state.banExpires),
1055 auth: UserService.Instance.authField(),
1057 WebSocketService.Instance.client.banUser(form);
1060 i.state.showBanDialog = false;
1061 i.setState(i.state);
1064 handleShowConfirmAppointAsMod(i: CommentNode) {
1065 i.state.showConfirmAppointAsMod = true;
1066 i.setState(i.state);
1069 handleCancelConfirmAppointAsMod(i: CommentNode) {
1070 i.state.showConfirmAppointAsMod = false;
1071 i.setState(i.state);
1074 handleAddModToCommunity(i: CommentNode) {
1075 let cv = i.props.node.comment_view;
1076 let form: AddModToCommunity = {
1077 user_id: cv.creator.id,
1078 community_id: cv.community.id,
1080 auth: UserService.Instance.authField(),
1082 WebSocketService.Instance.client.addModToCommunity(form);
1083 i.state.showConfirmAppointAsMod = false;
1084 i.setState(i.state);
1087 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1088 i.state.showConfirmAppointAsAdmin = true;
1089 i.setState(i.state);
1092 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1093 i.state.showConfirmAppointAsAdmin = false;
1094 i.setState(i.state);
1097 handleAddAdmin(i: CommentNode) {
1098 let form: AddAdmin = {
1099 user_id: i.props.node.comment_view.creator.id,
1101 auth: UserService.Instance.authField(),
1103 WebSocketService.Instance.client.addAdmin(form);
1104 i.state.showConfirmAppointAsAdmin = false;
1105 i.setState(i.state);
1108 handleShowConfirmTransferCommunity(i: CommentNode) {
1109 i.state.showConfirmTransferCommunity = true;
1110 i.setState(i.state);
1113 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1114 i.state.showConfirmTransferCommunity = false;
1115 i.setState(i.state);
1118 handleTransferCommunity(i: CommentNode) {
1119 let cv = i.props.node.comment_view;
1120 let form: TransferCommunity = {
1121 community_id: cv.community.id,
1122 user_id: cv.creator.id,
1123 auth: UserService.Instance.authField(),
1125 WebSocketService.Instance.client.transferCommunity(form);
1126 i.state.showConfirmTransferCommunity = false;
1127 i.setState(i.state);
1130 handleShowConfirmTransferSite(i: CommentNode) {
1131 i.state.showConfirmTransferSite = true;
1132 i.setState(i.state);
1135 handleCancelShowConfirmTransferSite(i: CommentNode) {
1136 i.state.showConfirmTransferSite = false;
1137 i.setState(i.state);
1140 handleTransferSite(i: CommentNode) {
1141 let form: TransferSite = {
1142 user_id: i.props.node.comment_view.creator.id,
1143 auth: UserService.Instance.authField(),
1145 WebSocketService.Instance.client.transferSite(form);
1146 i.state.showConfirmTransferSite = false;
1147 i.setState(i.state);
1150 get isCommentNew(): boolean {
1151 let now = moment.utc().subtract(10, 'minutes');
1152 let then = moment.utc(this.props.node.comment_view.comment.published);
1153 return now.isBefore(then);
1156 handleCommentCollapse(i: CommentNode) {
1157 i.state.collapsed = !i.state.collapsed;
1158 i.setState(i.state);
1161 handleViewSource(i: CommentNode) {
1162 i.state.viewSource = !i.state.viewSource;
1163 i.setState(i.state);
1166 handleShowAdvanced(i: CommentNode) {
1167 i.state.showAdvanced = !i.state.showAdvanced;
1168 i.setState(i.state);
1173 if (this.state.my_vote == 1) {
1175 } else if (this.state.my_vote == -1) {
1176 return 'text-danger';
1178 return 'text-muted';
1182 get pointsTippy(): string {
1183 let points = i18n.t('number_of_points', {
1184 count: this.state.score,
1187 let upvotes = i18n.t('number_of_upvotes', {
1188 count: this.state.upvotes,
1191 let downvotes = i18n.t('number_of_downvotes', {
1192 count: this.state.downvotes,
1195 return `${points} • ${upvotes} • ${downvotes}`;